玩转 Hexo 代码高亮:从官方方案到自定义功能的实现


大家好!今天我们来聊聊 Hexo 博客中一个非常重要但又常常让人头疼的功能——代码高亮。一篇技术文章,如果代码块乱糟糟的,那阅读体验简直是灾难。

在这篇文章里,我们会深入探讨 Hexo 官方支持的两种高亮库 highlight.jsprism.js,并分享一下我在为主题添加“代码块折叠”等自定义功能时踩过的坑和解决思路。

Hexo 的两大代码高亮引擎

首先,Hexo 官方文档告诉我们,它主要支持两个主流的代码高亮库:highlight.jsprism.js

1. highlight.js:默认的“老大哥”

highlight.js 是 Hexo 的默认选项,开箱即用。它本身功能很强大,但当 Hexo 为了实现一个常见功能——显示行号时,情况就变得有趣了。

为了加上行号,Hexo 会用 <figure><table> 标签把原始的代码块包裹起来,形成一个比较复杂的 HTML 结构。

开启行号后的 HTML 结构:

<figure class="highlight yaml">
<table>
<tbody>
<tr>
  <td class="gutter">
    <pre><span class="line">1</span><br></pre>
  </td>
  <td class="code">
    <pre><span class="line"><span class="attr">hello:</span><span class="string">hexo</span></span><br></pre>
  </td>
</tr>
</tbody>
</table>
</figure>

你看,左边是行号(gutter),右边是代码(code),泾渭分明。

这套结构带来的一个“副作用”就是,你从 highlight.js 官网直接下载的那些漂亮主题 CSS 文件,很可能就没法直接用了。因为官方主题的 CSS 选择器是为下面这种纯粹的结构设计的。

highlight.js 默认的 HTML 结构:

<pre><code class="yaml">
<span class="comment"># _config.yml</span>
<span class="attr">hexo:</span> <span class="string">hexo</span>
</code></pre>

怎么办呢?

如果你就是想用官方主题,又不想自己手动改 CSS 适配,唯一的办法就是“返璞归真”。修改你的站点配置文件 _config.yml

# _config.yml
highlight:
  enable: true
  line_number: false # 关闭行号
  wrap: false        # 关闭外部包裹
  hljs: true

当你把 line_numberwrap 都设为 false 后,Hexo 就会生成上面那种纯粹的 HTML 结构,这样官方的 CSS 就能无缝衔接了。当然,代价就是牺牲了行号显示。

2. prism.js:更现代的挑战者

prism.js 是另一款非常优秀的高亮库。要使用它,首先得在 _config.yml 里“换将”。

# _config.yml
highlight:
  enable: false # 先关闭默认的 highlight.js
prismjs:
  enable: true
  preprocess: true
  line_number: true # 开启行号

prism.js 的优点在于,即使开启行号,它也保持了 <pre><code> 这种简洁的 HTML 结构,这对于我们做二次开发或者自定义样式非常友好。

不过,它也有一个大坑,尤其是在行号显示上。我曾因为这个问题耗费了大量时间。

prism.js 行号对不齐问题

引入官方的行号插件和 CSS 后,你可能会发现行号和代码总也对不齐,像这样:

1   第一行代码
  2 第二行代码
3   第三行代码

究其原因,prism.js 是通过在代码末尾追加一个特殊的 <span> 标签来统一渲染行号的。如果你在 CSS 中给 pre 或者 code 标签设置了自定义的 line-height(行高)属性,就极有可能导致左侧代码的行高与右侧行号的行高计算不一致,从而引发错位。

解决方案:
最简单粗暴也最有效的方法就是——**不要给你的 pre 标签单独设置 line-height**。让它继承文章正文的行高,通常问题就能迎刃而解。

当两大引擎都关闭时:裸奔模式

如果你在 _config.yml 中把 highlight.enableprismjs.enable 都设置成了 false,那代码块会变成什么样呢?

它会输出一个最原始的 HTML 结构,没有任何用于高亮的 <span> 标签。

裸奔模式的 HTML 结构:

<pre><code class="yaml">
# _config.yml
hexo: hexo
</code></pre>

这种模式下,代码没有颜色区分,就是纯文本。好处是结构最干净,如果你想引入一个完全自定义的第三方高亮方案,可以从这个最原始的状态开始。

实战:为主题添加代码块折叠与语言显示

了解了上面的底层结构后,我们就可以轻松地添加一些自定义功能了。下面我分享一下为主题添加“代码块折叠”和“语言显示”功能的实现思路。

这个思路对 highlight.jsprism.js 都适用,只是需要对两种不同的 HTML 结构做一点适配。

1. 代码块折叠/隐藏

核心原理: 利用 CSS 的 display 属性,通过 JavaScript 来回切换 display: blockdisplay: none

实现步骤:

  1. JS 注入 HTML: 用 JavaScript 找到页面上所有的代码块容器(比如 <figure class="highlight">),在它前面动态插入一个工具栏 <div>,里面包含一个折叠按钮。

    // 伪代码,示意逻辑
    const highlightBlocks = document.querySelectorAll('.highlight');
    highlightBlocks.forEach(block => {
      const toolsDiv = document.createElement('div');
      toolsDiv.className = 'highlight-tools';
      toolsDiv.innerHTML = '<button class="toggle-btn">折叠</button>';
      block.parentNode.insertBefore(toolsDiv, block);
    });
  2. CSS 控制显示/隐藏: 我们给工具栏 <div> 添加一个状态类,比如 closed。当这个类存在时,就隐藏它后面的代码块。

    /* ~ 是 CSS 中的后续同胞组合器,表示“选择后面所有的兄弟元素” */
    .highlight-tools.closed ~ .highlight {
      display: none;
    }
  3. JS 绑定点击事件: 给折叠按钮添加点击事件。每次点击,就切换 highlight-tools 那个 <div> 是否拥有 closed 类。

    // 伪代码,示意逻辑
    const toggleButtons = document.querySelectorAll('.toggle-btn');
    toggleButtons.forEach(btn => {
      btn.addEventListener('click', () => {
        const toolsDiv = btn.parentElement;
        toolsDiv.classList.toggle('closed');
      });
    });

这样,一个简单的代码折叠功能就完成了。

2. 显示代码语言

核心原理: 代码块的容器元素上通常会有一个 class 来标识代码的语言,比如 class="highlight yaml"class="language-javascript"。我们用 JS 把它读出来就行。

实现步骤:

  1. JS 截取 Class: 在上一步注入工具栏 <div> 的同时,我们找到代码块容器。

    • 对于 highlight.js,语言在 <figure> 标签的 class 里(如 highlight yaml)。
    • 对于 prism.js,语言在 <code> 标签的 class 里(如 language-js)。
  2. 提取并显示: 用字符串处理方法提取出语言名称,然后把它也插入到工具栏里。

    // 伪代码,示意逻辑
    // ... 在注入工具栏的循环中 ...
    
    // 假设 block 是 <figure class="highlight yaml">
    let lang = '';
    const langMatch = Array.from(block.classList).find(cls => cls !== 'highlight');
    if (langMatch) {
      lang = langMatch;
    }
    
    // 如果是 prism.js,则需要查找 code 标签
    if (!lang) {
      const codeTag = block.querySelector('code[class*="language-"]');
      if (codeTag) {
        lang = codeTag.className.replace('language-', '');
      }
    }
    
    // 将语言显示出来
    const langSpan = document.createElement('span');
    langSpan.className = 'lang-name';
    langSpan.textContent = lang;
    toolsDiv.appendChild(langSpan);

通过这种方式,我们就能灵活地在页面上展示当前代码块所使用的编程语言了。

总结

Hexo 的代码高亮功能看似简单,但深入了解后会发现其底层 HTML 结构对我们的主题开发和自定义有很大影响。

  • **highlight.js**:默认选择,开启行号会改变 HTML 结构,可能与官方主题不兼容。
  • **prism.js**:结构更标准,但在处理行号时要特别小心 line-height 属性,避免样式错位。
  • 自定义功能:无论使用哪种库,只要我们理解了它们生成的 HTML 结构,就能通过 JavaScript 和 CSS 轻松地为其添加折叠、复制代码、显示语言等丰富的功能。

希望这篇文章能帮助你更好地驾驭 Hexo 的代码高亮,打造出既美观又实用的技术博客!


  目录