# 代码高亮实现原理详解
# 整体架构
代码高亮的实现涉及三个核心组件:
- markdown-it - Markdown 解析器
- highlight.js - 代码高亮引擎
- CSS 样式 - 视觉呈现
# 一、Markdown-it 的工作原理
# 1.1 Markdown-it 是什么?
markdown-it 是一个 Markdown 解析器,它将 Markdown 文本转换为 HTML。
# 1.2 基本工作流程
原始 Markdown 文本
↓
markdown-it 解析器
↓
AST (抽象语法树)
↓
渲染器 (Renderer)
↓
HTML 字符串
# 1.3 代码块的处理
当 markdown-it 遇到代码块时(例如 ````javascript` 或三个反引号),它会:
- 识别代码块:检测到代码块标记
- 提取内容:获取代码内容和语言标识
- 调用 highlight 函数:如果配置了
highlight选项,会调用这个函数 - 替换为 HTML:将函数返回的 HTML 插入到最终输出中
# 1.4 配置中的 highlight 选项
const md: MarkdownIt = new MarkdownIt({ | |
html: true, | |
linkify: true, | |
typographer: true, | |
highlight: function (str: string, lang?: string): string { | |
// 这个函数会在遇到代码块时被调用 | |
//str: 代码内容 | |
//lang: 语言标识(如 'javascript', 'python' 等) | |
// 返回值: HTML 字符串 | |
} | |
}) |
关键点:
highlight函数是 markdown-it 的钩子函数- 每当解析器遇到代码块,就会调用这个函数
- 函数接收原始代码字符串和语言标识
- 函数必须返回 HTML 字符串,这个字符串会替换原始的代码块
# 二、Highlight.js 的工作原理
# 2.1 Highlight.js 是什么?
highlight.js 是一个语法高亮库,它能够:
- 识别代码中的关键字、字符串、注释等语法元素
- 为这些元素添加 CSS 类名
- 通过 CSS 类名应用不同的颜色样式
# 2.2 核心 API
# 2.2.1 hljs.getLanguage(lang)
if (lang && hljs.getLanguage(lang)) { | |
// 检查 highlight.js 是否支持该语言 | |
} |
- 检查 highlight.js 是否支持指定的编程语言
- 返回语言定义对象或
undefined
# 2.2.2 hljs.highlight(str, options)
hljs.highlight(str, { | |
language: lang, | |
ignoreIllegals: true | |
}) |
- 输入:原始代码字符串和语言标识
- 处理:
- 使用该语言的语法规则进行词法分析
- 识别关键字、字符串、注释、函数名等
- 为每个语法元素添加
<span>标签和对应的 CSS 类名
- 输出:包含高亮标记的 HTML 字符串
示例转换过程:
// 输入 | |
const hello = "world"; | |
//highlight.js 处理后(简化版) | |
<span class="hljs-keyword">const</span> <span class="hljs-variable">hello</span> = <span class="hljs-string">"world"</span>; |
# 2.2.3 hljs.highlightAuto(str)
hljs.highlightAuto(str) |
- 自动检测代码的语言类型
- 返回检测结果和高亮后的 HTML
# 2.3 返回值的结构
{ | |
value: string, // 高亮后的 HTML 字符串 | |
language: string, // 检测到的语言 | |
relevance: number // 匹配度分数 | |
} |
# 三、两者如何结合
# 3.1 完整的处理流程
AI 回复的 Markdown 文本
↓
例如:
javascript
const x = 1;
↓
markdown-it 解析器识别到代码块
↓
调用 highlight 函数
↓
highlight.js 进行语法分析和高亮
↓
返回带高亮标记的 HTML
↓
markdown-it 将 HTML 插入最终输出
↓
浏览器渲染 HTML
↓
CSS 样式应用颜色
↓
用户看到高亮的代码
# 3.2 代码实现详解
highlight: function (str: string, lang?: string): string { | |
// 第一步:检查是否指定了语言且 highlight.js 支持 | |
if (lang && hljs.getLanguage(lang)) { | |
try { | |
// 使用指定语言进行高亮 | |
return ( | |
'<pre class="hljs"><code>' + | |
hljs.highlight(str, { language: lang, ignoreIllegals: true }).value + | |
'</code></pre>' | |
) | |
} catch { | |
// 如果高亮失败(例如代码有语法错误),继续执行下面的逻辑 | |
} | |
} | |
// 第二步:如果没有指定语言或语言不支持,尝试自动检测 | |
try { | |
return ( | |
'<pre class="hljs"><code>' + | |
hljs.highlightAuto(str).value + | |
'</code></pre>' | |
) | |
} catch { | |
// 第三步:如果自动检测也失败,返回转义的纯文本代码 | |
return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>' | |
} | |
} |
为什么需要三层 fallback?
- 第一层:用户明确指定语言,使用该语言的高亮规则(最准确)
- 第二层:自动检测语言(用户未指定时)
- 第三层:如果都失败,至少保证代码能安全显示(防止 XSS)
# 3.3 HTML 结构
最终生成的 HTML 结构:
<pre class="hljs"> | |
<code> | |
<span class="hljs-keyword">const</span> | |
<span class="hljs-variable">x</span> | |
<span class="hljs-operator">=</span> | |
<span class="hljs-number">1</span> | |
<span class="hljs-punctuation">;</span> | |
</code> | |
</pre> |
# 四、CSS 样式的作用
# 4.1 为什么需要 CSS?
highlight.js 只是添加了类名,实际的颜色是通过 CSS 实现的。
# 4.2 CSS 主题文件
import 'highlight.js/styles/github-dark.css' |
这个文件包含了所有语法元素的颜色定义:
.hljs-keyword { color: #ff7b72; } | |
.hljs-string { color: #a5d6ff; } | |
.hljs-number { color: #79c0ff; } | |
.hljs-comment { color: #8b949e; } | |
/* ... 更多样式 ... */ |
# 4.3 自定义样式增强
/* 代码块容器样式 */ | |
:deep(.markdown-body pre.hljs) { | |
padding: 16px; /* 内边距 */ | |
overflow-x: auto; /* 横向滚动 */ | |
border: 1px solid rgba(255, 255, 255, 0.1); /* 边框 */ | |
border-radius: 8px; /* 圆角 */ | |
margin-bottom: 1em; /* 底部间距 */ | |
} | |
/* 代码内容样式 */ | |
:deep(.markdown-body pre.hljs code) { | |
background-color: transparent !important; /* 透明背景 */ | |
font-family: 'SF Mono', 'Monaco', ...; /* 等宽字体 */ | |
line-height: 1.6; /* 行高 */ | |
white-space: pre; /* 保留空白 */ | |
} |
为什么需要 !important ?
- highlight.js 的主题 CSS 可能设置了背景色
- 我们需要覆盖它,让代码块使用统一的背景色
# 4.4 行内代码 vs 代码块
/* 行内代码(不包含 .hljs 类) */ | |
:deep(.markdown-body code:not(.hljs)) { | |
background-color: rgba(175, 184, 193, 0.2); | |
padding: 0.2em 0.4em; | |
/* 简单的灰色背景,不高亮 */ | |
} | |
/* 代码块(包含 .hljs 类) */ | |
:deep(.markdown-body pre.hljs) { | |
/* 完整的语法高亮样式 */ | |
} |
# 五、完整的执行示例
# 5.1 输入示例
这是一个 JavaScript 示例:
function greet(name) { | |
return `Hello, ${name}!`; | |
} |
# 5.2 处理过程
markdown-it 解析:
- 识别到代码块,语言是
javascript - 提取代码内容:
function greet(name) { ... } - 调用
highlight('function greet...', 'javascript')
- 识别到代码块,语言是
highlight.js 处理:
hljs.highlight('function greet(name) { ... }', {
language: 'javascript'
})
- 识别
function为关键字 - 识别
greet为函数名 - 识别
name为参数 - 识别字符串模板
- 识别
生成 HTML:
<pre class="hljs"><code><span class="hljs-keyword">function</span>
<span class="hljs-title function_">greet</span>
(<span class="hljs-params">name</span>) {
<span class="hljs-keyword">return</span>
<span class="hljs-string">`Hello, ${name}!`</span>;
}
</code></pre>CSS 应用:
.hljs-keyword→ 红色(#ff7b72).hljs-string→ 蓝色(#a5d6ff).hljs-title.function_→ 黄色- 等等...
最终渲染:
- 浏览器解析 HTML
- 应用 CSS 样式
- 用户看到彩色高亮的代码
# 六、关键技术点总结
# 6.1 为什么这样能工作?
markdown-it 的扩展机制:
highlight选项是 markdown-it 提供的插件接口- 允许开发者自定义代码块的处理方式
highlight.js 的语法分析:
- 使用词法分析器(Lexer)识别语法元素
- 每种语言都有对应的语法规则定义
- 通过正则表达式和状态机进行匹配
CSS 类名的约定:
- highlight.js 使用统一的类名规范(如
.hljs-keyword) - CSS 主题文件定义了这些类名的颜色
- 开发者可以切换主题或自定义样式
- highlight.js 使用统一的类名规范(如
# 6.2 性能考虑
- 缓存机制:highlight.js 会缓存已解析的语言定义
- 按需加载:可以只加载需要的语言包
- 异步处理:对于大文件,可以考虑异步高亮
# 6.3 安全性
- HTML 转义:
md.utils.escapeHtml(str)防止 XSS 攻击 - 错误处理:多层 fallback 确保即使出错也能显示代码
# 七、与 Gemini 效果的对比
# 7.1 相似之处
- ✅ 使用语法高亮库(Gemini 可能也使用类似方案)
- ✅ 深色主题(
github-dark) - ✅ 圆角边框和适当的内边距
- ✅ 自定义滚动条样式
- ✅ 等宽字体
# 7.2 可能的增强
- 代码复制按钮
- 行号显示
- 语言标识标签
- 代码折叠功能
# 总结
代码高亮的实现本质上是:
- 解析:markdown-it 识别代码块
- 分析:highlight.js 进行语法分析
- 标记:添加 HTML 标签和 CSS 类名
- 渲染:CSS 应用颜色样式
- 显示:浏览器渲染最终效果
整个过程是声明式的:我们定义了规则,库自动处理细节,最终得到美观的高亮代码。