Hexo-Next 主题修改 (2) -- 更换代码渲染器为 Shiki


边改主题边写博客,码代码码吐了就来写写博客,这是博客魔改计划中的第三篇,如题,本次的工作将是更换代码渲染器为 Shiki。

而更换的原因自然是自带的代码渲染器 highlightprismjs 太鶸了。

  • 从代码高亮的角度说,自带的代码渲染器对于 JS 来说还行,但对于 Bash Python Json 的高亮来说,不说是精美绝伦,也得是聊胜于无。
  • 从支持的语言数量来说,更类似于激光剑暴打原始人。
  • 而自带的代码渲染器更是完全没有暗色亮色主题的概念,而 Shiki 则是支持分离的主题文件,支持内联不同的主题。

Shiki 是什么?

Shiki(式,一个日语词汇,意为 “样式”) 是一款美观而强大的代码语法高亮器,它与 VS Code 的语法高亮引擎一样,基于 TextMate 的语法及主题。Shiki 能为几乎所有主流编程语言提供非常准确且快速的语法高亮。

根据 Shiki 的官方文档 Shiki 有以下特点:

  • 所有语法 / 主题 / WASM 都是纯 ESM,可以按需懒加载,对捆绑器友好
  • 高度通用,不依赖于 Node.js 的 API 和文件系统,可以在任何现代 JavaScript 运行时上运行
  • 默认仅支持 ESM,不过你依然可以 使用 CDN 或 使用 CJS
  • 语言与主题的细粒度捆绑
  • 深浅色模式支持
  • hast 支持
  • 转换器 API
  • 代码装饰 API
  • TypeScript Twoslash 集成
  • 兼容构建

Shiki 和 highlighter.js 的对比

By Shiki
// 将生成的 404 页面复制到根目录一份,方便在 vercel 提示 404 的时候显得不那么丑
hexo.extend.filter.register('before_exit', function() {
  const fs = require('fs');
  const path = require('path');
  // 检查目录是否存在
  if (!fs.existsSync(path.join(hexo.public_dir, '404'))) {
    return;
  }
  const source = path.join(hexo.public_dir, '404', 'index.html');
  const dest = path.join(hexo.public_dir, '404.html');
  fs.copyFileSync(source, dest);
});
By Highlighter
By Highlighter

更换代码渲染器为 Shiki

Shiki 作为一个纯异步项目,对于 Hexo 这种同步项目几乎就是灾难,以我在 Python 中对异步项目的了解,一步异步,步步异步应该也适用于 JS。

而对于我这种刚学一周 JS 的菜鸡来说,处理异步调用还是太困难了,所以就偷个懒将其转为纯粹的同步调用,虽然效率可能会打折扣,但影响不大。

所以,最简单的实现方式便是在注册一个 before_post_render 的 Hook,用正则匹配代码块,然后调用 Shiki 渲染代码即可完成。

/scripts/shiki/index.js
const shiki = require("shiki");
const stripIndent = require("strip-indent");

const codeMatch =
    /(?<quote>[> ]*)(?<ul>(-|\d+\.)?)(?<start>\s*)(?<tick>~{3,}|`{3,}) *(?<lang>\S+)? *(?<figcation>.*)?\n(?<code>[\s\S]*?)\k<quote>\s*\k<tick>(?<end>\s*)$/gm;

const config = hexo.config.shiki;

if (!config.enable) return;

let enabledThemes = new Set([config.theme]);
let { enable: darkModeEnabled, light, dark } = config.dark_mode;

if (darkModeEnabled) {
  enabledThemes.add(light);
  enabledThemes.add(dark);
}

return shiki.createHighlighter({
  themes: [...enabledThemes],
  langs: Object.keys(shiki.bundledLanguages),
})
    .then((hl) => {
      hexo.extend.filter.register("before_post_render", (post) => {
        post.content = post.content.replace(codeMatch, (...argv) => {
          let { quote, ul, start, end, lang, figcation, code } = argv.pop();
          lang = lang?.toLowerCase();
          let result;
          const match = new RegExp(`^${quote.trimEnd()}`, "gm");
          code = code.replace(match, "");
          code = stripIndent(code.trimEnd());
          let pre = "";
          try {
            if (darkModeEnabled) {
              pre = hl.codeToHtml(
                  code,
                  { lang: lang, themes: { light: 'github-light', dark: 'github-dark',} },
              );
            } else {
              pre = hl.codeToHtml(code, { lang: lang, theme: config.theme });
            }
            pre = pre.replace(/<pre[^>]*>/, (match) => {
              return match.replace(/\s*style\s*=\s*"[^"]*"\s*tabindex="0"/, "");
            });
          } catch (error) {
            if (error.name === "ShikiError" && error.message.includes(`Language \`${lang}\` not found`)) {
              hexo.log.warn(`Can't parse code block with language \`${lang}\` in \`${post.title}\`, fallback to plain text`);
            } else {
              hexo.log.error(error);
            }
            pre = `<pre>${code}</pre>`;
          }
          result = `<figure class="shiki${lang ? ` ${lang}` : ""}">`;
          if (figcation?.trim()) {
            result += `<figcaption>${figcation.trim()}</figcaption>`;
          }
          result += `${pre}</figure>`;
          return `${
              quote + ul + start
          }<hexoPostRenderCodeBlock>${result}</hexoPostRenderCodeBlock>${end}`;
        });
      });
    });

最后在 hexo.config.yml 中添加配置即可。

/_config.yml
shiki:
  enable: true

  dark_mode:
    enable: true
    light: github-light
    dark: github-dark

  theme: github-light

禁用或删除原先的代码渲染器

不管你之前使用的是 highlight 还是 prismjs,都需要禁用或删除原先的代码渲染器。
否则给电脑搞成电饭煲别怪我

操作非常简单,只需要将 syntax_highlighter 置为空,比如这样。

syntax_highlighter: 

Shiki 的 CSS

当然,CSS 必不可少,对于实现代码块的明暗色切换,还需要 CSS 的配合。此外,我将代码块的样式也进行了一些调整,使其更加美观,还加上了 figcation 的支持。

/* Shiki */
.shiki {
    line-height: 1.6em;
    margin: 0;
    padding: 12px;
}

/* 明暗色切换 */
[data-theme='dark'] .shiki,
[data-theme='dark'] .shiki span {
    color: var(--shiki-dark) !important;
}

/* figcaption 的父容器 */
figcaption {
    font-style: italic;
    font-size: 0.875em;
    position: relative;
    display: flex;
    justify-content: space-between;
    align-items: center;
    border-bottom: 1px solid var(--border-color);
    padding: 0 2px 0 10px;
    background-color: var(--img-figcaption-bg-color);
}

番外 — Shiki 转换器

Shiki 还支持非常多好玩的功能,比如转换器。当然,写这一部分的时候距离我贴出来的代码已经过了 114514 个版本,所以这里只是简单的介绍一下这个好玩的功能。

你需要自己来实现这部分,实现也非常简单,只需要在调用的时候加上转换器即可,可以参考 Shiki 官方教程—转换器

简单来说,这是一个解析被注释的代码,然后将特定行转为特定样式的功能。

比如 transformerNotationDiff 就提供了一个将 + 行渲染绿色,- 行渲染红色的功能,实现类似于 git diff 的效果。

import os  
import sys 
import re

transformerNotationHighlight 则提供了一个高亮显示行的功能。

console.log('Not highlighted')
console.log('Highlighted') // [!code highlight]
console.log('Not highlighted')

transformerNotationFocus 则提供了一个聚焦行的功能。此外,还可以使用 [!code focus:NUM] 来指定聚焦多行。

fn main() {
    println!("Hello, world!"); // [!code focus]
}

// [!code focus:4] 专注包括本行在内的接下来 4 行
fn is_leap(year: i32) -> bool {
    match {
        4 => true,
        100 => false,
        400 => true,
        _ => false,
    }
}

transformerNotationErrorLevel 则提供了一个渲染行为特定颜色的功能。

console.log('No errors or warnings')
console.error('Error') // [!code error]
console.warn('Warning') // [!code warning]


评论加载中……