markdown-it 代码块高亮进阶:行号显示与主题定制

markdown-it 代码块高亮进阶:行号显示与主题定制

【免费下载链接】markdown-it Markdown parser, done right. 100% CommonMark support, extensions, syntax plugins & high speed 【免费下载链接】markdown-it 项目地址: https://gitcode.com/gh_mirrors/ma/markdown-it

你是否在使用 markdown-it 时遇到过代码块难以阅读的问题?是否需要为代码添加行号来增强可读性?是否希望自定义代码块的样式以匹配项目主题?本文将系统讲解如何基于 markdown-it 实现代码块行号显示、主题定制和高亮优化,帮助你打造专业级的代码展示效果。

读完本文你将掌握:

  • 代码块行号生成的两种实现方案及性能对比
  • 自定义代码高亮主题的完整流程(含 5 种主流主题示例)
  • 代码块交互功能增强(行高亮、复制按钮、语言标识)
  • 生产环境优化策略(CDN 配置、懒加载、防 XSS)

代码块渲染原理与架构分析

markdown-it 通过插件化架构处理各种 Markdown 语法,代码块功能主要由 fence 规则和渲染器共同实现。

核心工作流程

mermaid) 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-resetcounter-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风格开源项目文档

自定义主题实现步骤

  1. 创建主题变量文件(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;
}
  1. 语法高亮样式(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; }
  1. 主题切换功能
// 主题切换逻辑
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代码块高亮的核心实现方案,包括:

  1. 行号显示:对比了CSS计数器和HTML生成两种方案的优缺点及适用场景
  2. 主题定制:提供了完整的主题变量定义和多主题切换实现
  3. 交互增强:实现了代码复制、折叠、行高亮等实用功能
  4. 性能优化:介绍了懒加载、XSS防护和缓存策略

进阶探索方向

  • 代码对比功能:实现多版本代码的差异高亮显示
  • 实时编辑预览:结合CodeMirror实现代码块在线编辑
  • 语法错误提示:集成ESLint等工具提供代码质量反馈
  • 代码执行环境:通过Web Worker实现代码块安全执行

通过合理运用这些技术,你可以为用户提供专业、高效的代码阅读体验,使技术文档更具吸引力和实用性。建议根据项目实际需求选择合适的实现方案,并关注代码性能和安全性的平衡。

要获取完整示例代码和更多主题资源,可以访问项目仓库:

git clone https://gitcode.com/gh_mirrors/ma/markdown-it

【免费下载链接】markdown-it Markdown parser, done right. 100% CommonMark support, extensions, syntax plugins & high speed 【免费下载链接】markdown-it 项目地址: https://gitcode.com/gh_mirrors/ma/markdown-it

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值