各位老友们好,我是 Chlorine。继续高强度水文。

本期的主题是 Hugo 的代码高亮,主要参考了蜗牛大神的教程:在 Hugo 中使用 Shiki。感谢前辈的付出。

前言

Hugo 的代码高亮基于 Chroma。这是个用 Go 语言写的库,好处是性能极高,坏处是不太聪明。比如我之前的文章 [[Hello,SearXNG|Hello,SearXNG]],里面有一段 PowerShell 脚本,结果:

An image to describe post

嗯……这个效果不能说是尽善尽美吧,至少也可以说是聊胜于无。

其他的部分,像 docker-composegit 之类的命令高亮不了,已经是紫荆园的麻辣香锅——家常便饭了。

那怎么修复?无非两个招:自己写 Lexer1,或者引入第三方库。

不清楚哪位勇士有第一项所需的娴熟的专业技能、大把的闲暇时间和超人的折腾勇气,反正小氯自认为没有。

Hugo 的第三方代码高亮库很多,比方说 Highlight.jsPrime.js,其优缺点可谓各有千秋。但它们都有一个致命的缺点:都需要在客户端执行大量的 JavaScript 代码。这就势必会对性能造成可怕的影响。如果大家没有什么感知的话,请参考下面的 Twikoo 的加载需要多久。

那有没有办法在服务端执行代码,把压力放在网站构建过程中呢?

有,方法就是我们今天的主角——Shiki。

Shiki 简介

Shiki 这个名字来自于日语的「式」(しき),意思就是「样式」。此外如果我记得没错,Shiki 应该还有季节、四季的意思,当然这不重要。

Shiki 是一个基于 VS Code 语法高亮引擎(没错,就是那个 VS Code)的代码高亮库。其基于 TextMate 语法定义文件和 WebAssembly 技术提供快速精确的代码高亮。

Shiki 很特别的一点在于:它在执行时会扫描指定路径的 HTML 文件,并且将各种语法 token 附加上内联样式。这使得 Shiki 非常适合用来为 Hugo 这样的静态网站构建工具添加高亮,无它,扫一遍 public 就行了。

顺便说一句:Shiki 是 Pine Wu 和 Anthony Fu 等前端大神的力作 :)

前置要求

请注意,如果您的已有条件和需求不满足下面所述,那小氯推荐您还是先别看文章了,还是先去紫荆公寓的公共空间喝杯茶更能体现对您生命的尊重。没错我是直接从上一篇文章复制过来的

条件:

  • 已经能用 Hugo 构建静态网站
  • 对 GitHub Actions 和 Vercel 等自动构建的流程有一定了解
  • 本地已经配置了一个 JavaScript 运行时及包管理器,例如 Node.js 或者 Bun.js。我们以下均使用 Bun.js 进行演示。

要求:

  • 对 Chroma 的高亮表现不满
  • 不希望使用 Highlight.js 等客户端高亮器

安装相关依赖

进入你的博客根目录,运行命令:

bun i shiki @shikijs/rehype rehype-cli

这应该会自动为你创建 node_modulesbun.lockbpackage.json。记得把它们加到 .gitignore 里面——如果没有自动添加的话。

配置 Hugo

修改 hugo.toml 或者你其他名字的配置文件,将 codeFences 改为 false。如果没有请自行创建,像这样:

[markup]
    [markup.highlight]
      codeFences = false

创建 .rehyperc

如果我没猜错的话,这应该是 rehype 的配置文件了。看起来跟个 JSON 似的。

{
  "plugins": [
    [
      "@shikijs/rehype",
      {
        "themes": {
          "light": "你的日间主题",
          "dark": "你的夜间主题"
        }
      }
    ]
  ]
}

主题列表在这里,挑个自己喜欢的吧。效果没必要像我一样每次都重新构建,在 VS Code 里面搜索同名主题看就行了。我比较选综,就用了 One Dark Pro 系列的。

rehype 这东西很神奇,可以对 HTML 各种爆改。当然,咱们这里用的就是其中一个插件,如果想进一步探索请看这里,如果有什么好的创意也可以和我分享,小氯洗耳恭听。

修改 Hugo CSS

找到你主题的 CSS。我们要做一下样式适配。我原本的样式大概是:

code {
    font-family: "Fira Code Light", "LXGW WenKai Lite", monospace;
}

code[class*="language-"],
pre[class*="language-"] {
    white-space: pre-wrap;
    /* 或 pre-line */
    word-break: break-all;
}

现在改成:

code {
    font-family: "Fira Code Light", "LXGW WenKai Lite", monospace;
}

html .shiki,
html .shiki span {
    white-space: pre-wrap;
    word-break: break-all;
    overflow-wrap: break-word;
    font-family: "Fira Code Light", "LXGW WenKai Lite", monospace;
}

html.dark .shiki,
html.dark .shiki span {
    color: var(--shiki-dark) !important;
    white-space: pre-wrap;
    word-break: break-all;
    overflow-wrap: break-word;
    font-family: "Fira Code Light", "LXGW WenKai Lite", monospace;
}

大家照猫画虎即可。

配置 bunx 脚本命令

在根目录的 package.json 下面添加这样的脚本命令:

"scripts": {
    "shiki": "bunx rehype-cli public -o"
}

大体而言改完之后你的文件应该看起来像是:

{
    "dependencies": {
        "@shikijs/rehype": "^1.13.0",
        "rehype-cli": "^12.0.0",
        "shiki": "^1.13.0"
    },
    "scripts": {
        "shiki": "bunx rehype-cli public -o"
    }
}

当然,shiki 这个名字你可以随便起。然后,你就可以在根目录下运行 bunx run shiki 对构建产物进行替换了。如果报错,大概率是因为你使用了不支持的语言或者错误的语言代码(比方说,html 写成 HTML)。可以在这里查看标准化的代号表。

Vercel 构建命令

GitHub Actions 版本的可以看上面蜗牛大神的教程。

cd themes/efimero && bun install && bun run build && cd ../.. && hugo --gc --minify && bun install && (bun run shiki || true)

这大概就是完整的构建命令了。最后的 || true 是为了防止报错导致的构建失败。

自动化

大家知道,小氯是个被 C++ 荼毒熏陶过的计算机系学生,因此习惯使用 Makefile 整合所有构建过程。

不过 Makefile 有个致命的缺点:只能一步一步执行命令。而 bunx run shiki 应该在 hugo server -D 构建 public 文件夹完成,但是未结束(只有我们手动停止这个命令才能结束) 时执行。使用后台运行的指令的话,又没办法准确把控时机。唯一的方法可能就是使用 fswatch 了,但是这样又会造成不必要的性能开销。

对于不经常折腾主题,只是发布内容的老友来说,这其实不是问题,主要对于小氯这样的喜欢折腾主题的人比较麻烦。现在只能是每次手动执行下,或者干脆只 make,缺点就是高亮没了。

去掉行号

在本地测试的时候我发现 Hugo 代码块的行号变得非常抽象,具体来说,10 居然会换行变成 1 和 0。改 hugo.toml 不管用,于是采取最简单粗暴的方法:CSS 隐藏。

.custom-md code span.line:before {
    display: none;
}

结语

又水了篇文章,内心毫无波兰波澜。

Shiki 的效果确实出挑,虽然会显著加长构建时间,但是也算可以接受。这样代码高亮看起来就好看多了。

以及一个新消息:我准备为主域名重新备案啦!


  1. Lexer,即词法分析器或扫描器,是编译器的第一阶段。其任务是读取源代码文本,并将其分解成一系列的标记(tokens)。每个标记代表了源代码中的一个有意义的片段,比如关键字、标识符、字面量、运算符等。Lexer 的输出是源代码的抽象表示,它为后续的语法分析(parser)提供了基础。 ↩︎