Prism.js 代码高亮原理
Prism.js 实现代码高亮的原理,其实就是一个 “字符串扫描 + 正则匹配 + 包裹标记” 的过程,
核心并不是依赖浏览器本身,而是通过语言语法定义 + 正则解析实现的。
简单流程就是
代码 → 正则匹配 → Token 树 → HTML 包裹 → CSS 上色。
语言语法定义(Grammar)
1.首先内部会定于关键字与正则的关系做一套“语法规则” grammar ,用对象的形式表示:
分为几个大类,作为语法规则
注释、字符串、关键字、常量、数值、运算符、符号、函数、标识符、对象、属性等等,例如:
Prism.languages.javascript = {
'comment': ///.*/g,
'string': /(["'])(?:(?!\1)[^\]|\.)*\1/g,
'keyword': /\b(?:if|else|for|while|function)\b/g,
'number': /\b\d+(?:.\d+)?\b/g,
'operator': /[+-*/=<>!&|]/g,
};
// 可以内联式嵌套 , 比如 <script> 就会去识别js语法
这里我们常说的扩展,自定义语言主要就是在这里进行补充书写,并且不仅支持简单的正则,还可以用对象形式配置高级规则,例如 greedy(贪婪匹配)、lookbehind(回溯匹配)、inside(嵌套子语法)。
解析过程(Tokenizer)
Tokenizer 就是一个 循环 + 切分 的过程: 输入字符串 → 按规则正则匹配 → 切分成 Token 或原始文本 → 重复直到整个字符串都处理完。
假设当前 token 是 "if (x > 10) console.log("hi");":
-
先用 keyword 正则
/\b(?:if|else|for|while|function)\b/去找 → 匹配"if" -
Prism 会把原 token 切分成三部分:
"if"(匹配部分 → 转换为Token("keyword", "if"))" "(if 后面的空格,没匹配 → 普通字符串)"(x > 10) console.log("hi");"(剩余部分)
此时 Token("keyword", "if") 被提取出来作为一个单位,其他部分继续通过语法规则进行递归地重新扫描,由于解析的过程是递归套用规则,所以对在超大代码文件中有性能瓶颈。
最终 Token 树
经过多轮扫描后,得到的 Token 列表类似:
[ Token("keyword", "if"), " ", Token("punctuation", "("), "x", " ", Token("operator", ">"), " ", Token("number", "10"), Token("punctuation", ")"), " ", Token("function", "console.log"), Token("punctuation", "("), Token("string", ""hi""), Token("punctuation", ")"), Token("punctuation", ";")]
- 未匹配部分 → 原样保留(普通字符串)
- 匹配部分 → 包装成
Token对象- 切分规则 → 一旦某个正则匹配成功,Prism 会把 token 拆开,插入 Token 对象,再继续从剩余部分继续匹配
- 递归 → 某些语言(比如 Markdown 内嵌代码)会递归调用不同 grammar
渲染过程(wrap HTML)
Prism 把上面的 token 树转换为 HTML,核心就是 Token.stringify:
然后根据生成的Token树 生成dom key作为样式 value作为code
return `<span class="token ${type}">${content}</span>`;
最终的高亮效果依赖 CSS 主题文件(如 prism.css)。开发者可以自定义 .token.keyword { ... } 来修改配色。
其他高亮库对比
Highlight.js
是通过详细写出关键字列表、模式、作用域,通过官方不断扩展Grammar。 并且按照流式处理,不会反复回溯。
更像一个 有限状态机,详细的Grammar配合流式处理可以解决臃长代码,但是规则被写死,灵活性差
Monaco / CodeMirror
这两者 在词性分析 、 语法解释上做了更为强大的算法处理方式(Lezer 语法树、虚拟渲染 、 增量解析等),以及对语法协议的封装(LSP)等等,一系列的优化,成为了一个虚拟机处理代码语法,就导致更为重
| 特性 | Prism.js | Highlight.js | Monaco / CodeMirror |
|---|---|---|---|
| 定位 | 轻量级代码高亮工具 | 通用型代码高亮工具 | 编辑器级语法解析/IDE |
| 解析原理 | 基于正则的递归 Tokenizer | 基于正则,自动语言检测 | 基于词法/语法解析器 |
| 语言支持 | 手动定义 grammar,可扩展 | 内置 180+ 语言,规则写死 | 内置解析器,支持主流语言,且可扩展 |
| 扩展性 | ⭐⭐⭐⭐(支持 inside / greedy / lookbehind,灵活) | ⭐⭐(支持度低,新增语言麻烦) | ⭐⭐⭐⭐(可写自定义语言服务) |
| 性能 | 适合小片段(博客/文档) | 中等(比 Prism 稍慢) | 适合大文件(IDE 场景) |
| 自动检测语言 | ❌ 需要手动指定 | ✅ 自动识别 | ❌ 通常需要指定语言模式 |
| 文件大小 | 很小(核心 ~2KB + 语言包) | 中等(几十 KB) | 较大(几百 KB ~ MB) |
| 应用场景 | 博客、文档、Markdown 渲染 | 通用展示(论坛、Wiki、CMS) | 在线编辑器、IDE、开发工具 |
| 典型使用者 | VuePress / Docusaurus 等文档站 | StackOverflow / Discourse | VS Code / Jupyter / GitHub Codespaces |
总结
Prism.js 的代码高亮流程可以概括为:
- Grammar 定义:用正则/对象定义语言的语法规则。
- Tokenizer 解析:递归扫描源码 → 生成 Token 树。
- HTML 渲染:
Token.stringify把 Token 树转为带 class 的 HTML。 - CSS 上色:Prism 的主题 CSS 根据
.token.xxx来着色。
核心原理就是 基于正则的递归 Tokenization + HTML 包裹,并通过可扩展 Grammar 实现多语言支持。