边改主题边写博客,码代码码吐了就来写写博客,这是博客魔改计划中的第三篇,如题,本次的工作将是更换代码渲染器为 Shiki。
而更换的原因自然是自带的代码渲染器 highlight 和 prismjs 太鶸了。
- 从代码高亮的角度说,自带的代码渲染器对于
JS来说还行,但对于BashPythonJson的高亮来说,不说是精美绝伦,也得是聊胜于无。 - 从支持的语言数量来说,更类似于激光剑暴打原始人。
- 而自带的代码渲染器更是完全没有暗色亮色主题的概念,而 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 的对比
// 将生成的 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);
});
更换代码渲染器为 Shiki
Shiki 作为一个纯异步项目,对于 Hexo 这种同步项目几乎就是灾难,以我在 Python 中对异步项目的了解,一步异步,步步异步应该也适用于 JS。
而对于我这种刚学一周 JS 的菜鸡来说,处理异步调用还是太困难了,所以就偷个懒将其转为纯粹的同步调用,虽然效率可能会打折扣,但影响不大。
所以,最简单的实现方式便是在注册一个 before_post_render 的 Hook,用正则匹配代码块,然后调用 Shiki 渲染代码即可完成。
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 中添加配置即可。
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 retransformerNotationHighlight 则提供了一个高亮显示行的功能。
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]