markdown-it 代码块高亮进阶:行号显示与主题定制
你是否在使用 markdown-it 时遇到过代码块难以阅读的问题?是否需要为代码添加行号来增强可读性?是否希望自定义代码块的样式以匹配项目主题?本文将系统讲解如何基于 markdown-it 实现代码块行号显示、主题定制和高亮优化,帮助你打造专业级的代码展示效果。
读完本文你将掌握:
- 代码块行号生成的两种实现方案及性能对比
- 自定义代码高亮主题的完整流程(含 5 种主流主题示例)
- 代码块交互功能增强(行高亮、复制按钮、语言标识)
- 生产环境优化策略(CDN 配置、懒加载、防 XSS)
代码块渲染原理与架构分析
markdown-it 通过插件化架构处理各种 Markdown 语法,代码块功能主要由 fence 规则和渲染器共同实现。
核心工作流程
) FenceRule->>Parser: 返回token对象 Parser->>Renderer: 调用fence渲染规则 Renderer->>Highlighter: 传入代码内容和语言 Highlighter-->>Renderer: 返回高亮HTML Renderer-->>Parser: 返回完整代码块HTML
### fence规则核心代码解析
```javascript
// lib/rules_block/fence.mjs 核心逻辑
function fence(state, startLine, endLine, silent) {
// 1. 检测代码块起始标记(```或~~~)
const marker = state.src.charCodeAt(pos);
if (marker !== 0x7E/* ~ */ && marker !== 0x60 /* ` */) return false;
// 2. 验证标记长度(至少3个连续字符)
let len = pos - mem;
if (len < 3) return false;
// 3. 提取语言信息和代码内容
const params = state.src.slice(pos, max); // 语言信息(如javascript)
const content = state.getLines(startLine + 1, nextLine, len, true); // 代码内容
// 4. 创建token对象
const token = state.push('fence', 'code', 0);
token.info = params; // 存储语言信息
token.content = content; // 存储代码内容
}
默认渲染器行为
// lib/renderer.mjs fence渲染规则
default_rules.fence = function (tokens, idx, options, env, slf) {
const token = tokens[idx];
const info = token.info ? unescapeAll(token.info).trim() : '';
let langName = '';
if (info) {
[langName] = info.split(/\s+/); // 提取语言名称
}
// 调用高亮函数处理代码
let highlighted;
if (options.highlight) {
highlighted = options.highlight(token.content, langName) || escapeHtml(token.content);
} else {
highlighted = escapeHtml(token.content);
}
// 生成最终HTML
return `<pre><code${slf.renderAttrs(tmpToken)}>${highlighted}</code></pre>\n`;
};
行号显示实现方案
方案一:CSS计数器实现(零JS依赖)
实现原理:利用CSS counter-reset 和 counter-increment 属性自动生成行号,通过伪元素 ::before 显示。
/* 行号样式 */
pre[class*="language-"] {
position: relative;
padding-left: 3.8em;
counter-reset: linenumber;
}
pre[class*="language-"] > code {
position: relative;
}
/* 行号生成 */
pre[class*="language-"] .line::before {
content: counter(linenumber);
counter-increment: linenumber;
position: absolute;
left: 0;
width: 3em;
padding-right: 0.8em;
text-align: right;
color: #999;
border-right: 1px solid #ddd;
}
JavaScript集成:需要修改渲染器将代码按行拆分并添加行标记:
// 自定义fence渲染器
function customFenceRenderer(tokens, idx, options, env, slf) {
const originalResult = slf.rules.fence(tokens, idx, options, env, slf);
// 仅对有语言指定的代码块添加行号
if (tokens[idx].info.trim()) {
return originalResult.replace(
/<code([^>]*)>/,
'<code$1><span class="line">')
.replace(/<\/code>/, '</span></code>')
.replace(/\n/g, '</span>\n<span class="line">');
}
return originalResult;
}
// 应用自定义渲染器
md.renderer.rules.fence = customFenceRenderer;
优缺点分析:
| 优点 | 缺点 |
|---|---|
| 纯CSS实现,无JS性能开销 | 无法实现行号点击、复制单行等交互功能 |
| 响应式布局友好 | 打印样式需要额外处理 |
| 不依赖代码高亮实现方式 | 行高不一致时可能导致错位 |
方案二:HTML行号生成(功能丰富)
实现原理:在代码高亮过程中生成包含行号的HTML结构,通常与代码高亮库配合使用。
Prism.js集成示例:
import Prism from 'prismjs';
import 'prismjs/components/prism-javascript';
import 'prismjs/themes/prism-okaidia.css';
// 配置markdown-it
const md = require('markdown-it')({
highlight: function (str, lang) {
if (lang && Prism.languages[lang]) {
try {
// 使用Prism高亮并添加行号
return Prism.highlight(str, Prism.languages[lang], lang)
.split('\n')
.map((line, i) => `<div class="line"><span class="line-number">${i+1}</span>${line}</div>`)
.join('\n');
} catch (__) {}
}
return '';
}
});
配套CSS:
.line {
position: relative;
padding-left: 4em;
}
.line-number {
position: absolute;
left: 0;
width: 3em;
padding-right: 0.5em;
text-align: right;
color: #666;
user-select: none;
}
性能对比:
| 指标 | CSS计数器 | HTML行号 |
|---|---|---|
| 初始渲染时间 | 快(DOM少30-50%) | 中(额外DOM节点) |
| 滚动性能 | 优(纯CSS定位) | 良(需优化重排) |
| 代码复制 | 需处理行号过滤 | 可选择性复制 |
| 长代码块(>100行) | 无明显延迟 | 可能有渲染阻塞 |
主题定制与样式优化
主流代码高亮主题对比
| 主题名称 | 特点 | 适用场景 | 对比度 |
|---|---|---|---|
| Default | 经典白底黑字,语法色彩适中 | 通用文档 | 中 |
| Dark (Dracula) | 深紫背景,高饱和度语法色 | 深色模式网站 | 高 |
| Solarized | 低饱和度,护眼配色 | 长时间阅读 | 低 |
| Monokai | 鲜明对比,适合屏幕阅读 | 代码演示 | 高 |
| Github | 模拟GitHub风格 | 开源项目文档 | 中 |
自定义主题实现步骤
- 创建主题变量文件(variables.css):
:root {
--code-bg: #f5f5f5;
--code-color: #333;
--line-number-color: #999;
--keyword: #07a;
--string: #0c8;
--comment: #998;
--function: #dd4a68;
--operator: #9a6e3a;
}
/* 深色模式变量 */
[data-theme="dark"] {
--code-bg: #1e1e1e;
--code-color: #d4d4d4;
--line-number-color: #666;
--keyword: #569cd6;
--string: #ce9178;
--comment: #6a9955;
--function: #dcdcaa;
--operator: #d4d4d4;
}
- 语法高亮样式(theme.css):
/* 基础样式 */
pre[class*="language-"] {
background: var(--code-bg);
color: var(--code-color);
border-radius: 6px;
padding: 1.5em;
font-family: 'Fira Code', 'Courier New', monospace;
font-size: 0.95em;
line-height: 1.5;
}
/* 语法元素样式 */
.token.keyword { color: var(--keyword); }
.token.string { color: var(--string); }
.token.comment { color: var(--comment); font-style: italic; }
.token.function { color: var(--function); }
.token.operator { color: var(--operator); }
.token.punctuation { color: var(--code-color); }
.token.number { color: #b5cea8; }
.token.boolean { color: #569cd6; }
.token.class-name { color: #4ec9b0; }
- 主题切换功能:
// 主题切换逻辑
function toggleCodeTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('code-theme', theme);
}
// 初始化主题
document.addEventListener('DOMContentLoaded', () => {
const savedTheme = localStorage.getItem('code-theme') || 'light';
toggleCodeTheme(savedTheme);
});
高级样式优化技巧
1. 行高亮效果:
/* 鼠标悬停行高亮 */
pre[class*="language-"] .line:hover {
background-color: rgba(255, 255, 255, 0.1);
transition: background-color 0.2s ease;
}
/* 特定行标记 */
pre[class*="language-"] .line.highlight {
background-color: rgba(255, 255, 0, 0.2);
border-left: 3px solid #ffd700;
}
2. 语言标识标签:
/* 语言标识样式 */
pre[class*="language-"]::before {
content: attr(data-language);
position: absolute;
top: 0;
right: 0;
padding: 0.2em 0.5em;
font-size: 0.7em;
background-color: rgba(0, 0, 0, 0.1);
border-radius: 0 0 0 4px;
color: var(--code-color);
}
功能增强与交互体验
代码复制功能
实现方案:结合Clipboard.js实现一键复制功能:
// 添加复制按钮
document.querySelectorAll('pre[class*="language-"]').forEach(block => {
const button = document.createElement('button');
button.className = 'copy-code-button';
button.textContent = '复制代码';
button.title = '点击复制代码到剪贴板';
block.appendChild(button);
// 复制逻辑
button.addEventListener('click', () => {
const code = block.querySelector('code').innerText;
navigator.clipboard.writeText(code).then(() => {
button.textContent = '复制成功!';
setTimeout(() => {
button.textContent = '复制代码';
}, 2000);
}).catch(err => {
button.textContent = '复制失败';
});
});
});
按钮样式:
.copy-code-button {
position: absolute;
top: 0.5em;
right: 0.5em;
padding: 0.3em 0.6em;
background-color: rgba(0, 0, 0, 0.5);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.7em;
opacity: 0;
transition: opacity 0.3s ease;
}
pre[class*="language-"]:hover .copy-code-button {
opacity: 1;
}
.copy-code-button:hover {
background-color: rgba(0, 0, 0, 0.7);
}
代码折叠功能
实现原理:通过检测代码块长度,自动为长代码块添加折叠功能:
// 代码折叠实现
document.querySelectorAll('pre[class*="language-"]').forEach(block => {
const codeLines = block.textContent.split('\n').length;
// 超过20行自动折叠
if (codeLines > 20) {
block.classList.add('collapsible');
const toggle = document.createElement('button');
toggle.className = 'toggle-code-button';
toggle.textContent = '展开代码';
block.appendChild(toggle);
toggle.addEventListener('click', () => {
block.classList.toggle('expanded');
toggle.textContent = block.classList.contains('expanded') ?
'收起代码' : '展开代码';
});
}
});
折叠样式:
pre.collapsible {
max-height: 200px;
overflow: hidden;
transition: max-height 0.3s ease;
}
pre.collapsible.expanded {
max-height: 2000px;
}
.toggle-code-button {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
padding: 0.3em;
background: linear-gradient(to top, rgba(0,0,0,0.7), transparent);
color: white;
border: none;
cursor: pointer;
}
生产环境配置与优化
国内CDN资源配置
<!-- markdown-it及相关插件 -->
<script src="https://cdn.jsdelivr.net/npm/markdown-it@13.0.1/dist/markdown-it.min.js"></script>
<!-- 代码高亮 -->
<script src="https://cdn.bootcdn.net/ajax/libs/prism/1.29.0/prism.min.js"></script>
<link href="https://cdn.bootcdn.net/ajax/libs/prism/1.29.0/themes/prism.min.css" rel="stylesheet">
<!-- 语言支持 -->
<script src="https://cdn.bootcdn.net/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/prism/1.29.0/components/prism-html.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/prism/1.29.0/components/prism-css.min.js"></script>
性能优化策略
1. 代码高亮懒加载:
// 仅对可见区域的代码块进行高亮
function lazyHighlightCodeBlocks() {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const codeBlock = entry.target;
const lang = codeBlock.getAttribute('data-language');
const code = codeBlock.textContent;
if (lang && Prism.languages[lang]) {
codeBlock.innerHTML = Prism.highlight(
code,
Prism.languages[lang],
lang
);
}
observer.unobserve(codeBlock);
}
});
});
// 观察所有未高亮的代码块
document.querySelectorAll('pre:not(.highlighted) code').forEach(block => {
observer.observe(block);
});
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', lazyHighlightCodeBlocks);
2. XSS防护:
// 使用DOMPurify净化HTML输出
import DOMPurify from 'dompurify';
// 配置markdown-it
const md = markdownit({
highlight: function(str, lang) {
// ... 高亮处理 ...
// 净化HTML防止XSS攻击
return DOMPurify.sanitize(highlightedCode);
}
});
3. 代码块缓存:
// 使用localStorage缓存已处理的代码块
const codeCache = new Map();
function renderCodeBlock(code, lang) {
const cacheKey = `${lang}:${btoa(code)}`;
// 检查缓存
if (codeCache.has(cacheKey)) {
return codeCache.get(cacheKey);
}
// 渲染代码块
const result = md.render(`\`\`\`${lang}\n${code}\n\`\`\``);
// 存入缓存(限制大小)
if (codeCache.size > 100) {
const oldestKey = codeCache.keys().next().value;
codeCache.delete(oldestKey);
}
codeCache.set(cacheKey, result);
return result;
}
完整实现示例
前端集成代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>markdown-it代码块高亮示例</title>
<!-- 代码高亮主题 -->
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css">
<!-- 自定义样式 -->
<style>
/* 行号和代码样式 */
pre[class*="language-"] {
position: relative;
padding: 1.5em 1em 1.5em 4em;
counter-reset: linenumber;
border-radius: 8px;
overflow-x: auto;
}
.line {
position: relative;
}
.line::before {
content: counter(linenumber);
counter-increment: linenumber;
position: absolute;
left: -3em;
width: 2.5em;
text-align: right;
color: #888;
border-right: 1px solid #555;
padding-right: 0.5em;
}
/* 复制按钮 */
.copy-btn {
position: absolute;
top: 0.5em;
right: 0.5em;
padding: 0.3em 0.6em;
background: rgba(0,0,0,0.5);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.8em;
}
</style>
</head>
<body>
<div id="content"></div>
<!-- 引入依赖 -->
<script src="https://cdn.jsdelivr.net/npm/markdown-it@13.0.1/dist/markdown-it.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/prism/1.29.0/prism.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js"></script>
<script>
// 初始化markdown-it
const md = window.markdownit({
html: true,
linkify: true,
typographer: true,
highlight: function(str, lang) {
if (lang && Prism.languages[lang]) {
try {
// 使用Prism高亮代码
const highlighted = Prism.highlight(str, Prism.languages[lang], lang);
// 按行拆分并添加行标记
const lines = highlighted.split('\n')
.map(line => `<div class="line">${line}</div>`)
.join('\n');
// 净化HTML防止XSS
return DOMPurify.sanitize(lines);
} catch (err) {
console.error('代码高亮失败:', err);
}
}
// 默认处理
return DOMPurify.sanitize(str);
}
});
// 示例Markdown内容
const markdownContent = `
## JavaScript示例代码
\`\`\`javascript
/**
* 斐波那契数列生成函数
* @param {number} n - 数列长度
* @returns {number[]} 斐波那契数列
*/
function fibonacci(n) {
if (n <= 0) return [];
if (n === 1) return [0];
const sequence = [0, 1];
for (let i = 2; i < n; i++) {
sequence[i] = sequence[i - 1] + sequence[i - 2];
}
return sequence;
}
// 生成前10项斐波那契数列
const result = fibonacci(10);
console.log(result); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
\`\`\`
## HTML示例代码
\`\`\`html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>示例页面</title>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>
\`\`\`
`;
// 渲染Markdown并插入到页面
const container = document.getElementById('content');
container.innerHTML = md.render(markdownContent);
// 添加复制按钮
document.querySelectorAll('pre').forEach(block => {
const button = document.createElement('button');
button.className = 'copy-btn';
button.textContent = '复制';
block.appendChild(button);
button.addEventListener('click', () => {
const code = block.textContent;
navigator.clipboard.writeText(code).then(() => {
button.textContent = '已复制';
setTimeout(() => button.textContent = '复制', 2000);
});
});
});
</script>
</body>
</html>
总结与进阶方向
本文详细介绍了markdown-it代码块高亮的核心实现方案,包括:
- 行号显示:对比了CSS计数器和HTML生成两种方案的优缺点及适用场景
- 主题定制:提供了完整的主题变量定义和多主题切换实现
- 交互增强:实现了代码复制、折叠、行高亮等实用功能
- 性能优化:介绍了懒加载、XSS防护和缓存策略
进阶探索方向
- 代码对比功能:实现多版本代码的差异高亮显示
- 实时编辑预览:结合CodeMirror实现代码块在线编辑
- 语法错误提示:集成ESLint等工具提供代码质量反馈
- 代码执行环境:通过Web Worker实现代码块安全执行
通过合理运用这些技术,你可以为用户提供专业、高效的代码阅读体验,使技术文档更具吸引力和实用性。建议根据项目实际需求选择合适的实现方案,并关注代码性能和安全性的平衡。
要获取完整示例代码和更多主题资源,可以访问项目仓库:
git clone https://gitcode.com/gh_mirrors/ma/markdown-it
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



