Prism.js 代码高亮原理

71 阅读4分钟

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 切分成三部分

    1. "if"(匹配部分 → 转换为 Token("keyword", "if")
    2. " "(if 后面的空格,没匹配 → 普通字符串)
    3. "(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", ";")]
  1. 未匹配部分 → 原样保留(普通字符串)
  2. 匹配部分 → 包装成 Token 对象
  3. 切分规则 → 一旦某个正则匹配成功,Prism 会把 token 拆开,插入 Token 对象,再继续从剩余部分继续匹配
  4. 递归 → 某些语言(比如 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.jsHighlight.jsMonaco / CodeMirror
定位轻量级代码高亮工具通用型代码高亮工具编辑器级语法解析/IDE
解析原理基于正则的递归 Tokenizer基于正则,自动语言检测基于词法/语法解析器
语言支持手动定义 grammar,可扩展内置 180+ 语言,规则写死内置解析器,支持主流语言,且可扩展
扩展性⭐⭐⭐⭐(支持 inside / greedy / lookbehind,灵活)⭐⭐(支持度低,新增语言麻烦)⭐⭐⭐⭐(可写自定义语言服务)
性能适合小片段(博客/文档)中等(比 Prism 稍慢)适合大文件(IDE 场景)
自动检测语言❌ 需要手动指定✅ 自动识别❌ 通常需要指定语言模式
文件大小很小(核心 ~2KB + 语言包)中等(几十 KB)较大(几百 KB ~ MB)
应用场景博客、文档、Markdown 渲染通用展示(论坛、Wiki、CMS)在线编辑器、IDE、开发工具
典型使用者VuePress / Docusaurus 等文档站StackOverflow / DiscourseVS Code / Jupyter / GitHub Codespaces

总结

Prism.js 的代码高亮流程可以概括为:

  1. Grammar 定义:用正则/对象定义语言的语法规则。
  2. Tokenizer 解析:递归扫描源码 → 生成 Token 树。
  3. HTML 渲染Token.stringify 把 Token 树转为带 class 的 HTML。
  4. CSS 上色:Prism 的主题 CSS 根据 .token.xxx 来着色。

核心原理就是 基于正则的递归 Tokenization + HTML 包裹,并通过可扩展 Grammar 实现多语言支持。