关于 Mermaid
因为图表和文档会耗费开发者宝贵的时间,并且很快就会过时。但是,没有图表或文档会破坏生产力并损害组织学习。
所以 Mermaid 应用而生,Mermaid 允许你使用文本和代码创建图表和可视化。它是一个基于 JavaScript 的图表绘制工具,可渲染 Markdown 启发的文本定义以动态创建和修改图表。
序
在 Astro GitHub 的 Issue 中,有人提出了 Astro 不支持 Mermaid 的问题 #4433。
下面也有赛博菩萨贴出来了自己的解决方案,动态的引入 Mermaid JS 来解决。
但是在 Astro 这种静态网站生成框架的背景下,在能不传给客户端 JS 的前提下绝对不应该传递给客户端 JS,所有的页面渲染都应该在服务端完成。
所以我决定在不在客户端引入额外 JS 的情况下,为 Astro 增加 Mermaid 支持。
理解 Astro 渲染流程
上图是 Astro 渲染 MD 的简化流程,实际上还有许多其他的渲染器和包装器在起作用,但这里我只展示了最重要的一些。
此外,Remark 和 Rehype 都是基于 unified 架构的抽象语法树转换器,它们内部还有许多流程和插件,上图也没有展示,你可以去阅读 unified 官方 GitHub 中的说明。
总体来说,一篇 MD 渲染为 HTML 的流程是:
- Astro 读取 MD 文件为字符串,剔除 Front Matter 并作为 Global Config。
- Astro 通过
Remark将 MD 字符串转换为mdast。这是一个针对 MD 的抽象语法树。 - Astro 通过
Remark-rehype将mdast转换为hast。这是一个针对 HTML 的抽象语法树。 - 如果没有禁用 Shiki 的高亮,Astro 会将
hast传给rehype-shiki插件,将代码块转换为高亮的 HTML。 - 运行配置文件中写的其他 rehype 插件
- 使用
rehype-stringify将hast转为 HTML,并结合用户的 Layout 模板并生成最终的 HTML。
构思实现方式
所以,要支持 MD 语法的 Mermaid,最好的实现便是在上述的第二步和第三步之间加入一个插件,直接将带有 Mermaid
语法的代码块转为一个带图片的 hast 节点,并抛弃原有的内容。 这样代码块便不会被 Shiki 再次渲染变为 <pre> 块。
所以第一考虑便是采用 remark-mermaidjs 库来实现。
但非常致命的是它没有暗黑模式支持,至少不在插件层面提供暗黑模式支持。Oh damn,你能想象得到黑底黑字的 Mermaid 是什么样子吗?
所以被迫只能采用 rehype 插件的方式来实现,但又有一个非常致命的问题。Shiki 的渲染在 rehype 之前,
意味着 ryhype-mermaid 拿到的代码块都是被 Shiki 渲染过的,虽然可以通过自己的插件再转回来,但这样的做法实在是太蠢了,而且代码量爆炸。
查看 Astro 官方配置文档
发现可以手动禁用掉代码块渲染。这样我们就可以在 rehype 之前拿到原始的代码块,在渲染 Mermaid 之后再手动引入 Shiki 完成渲染。
实现
禁用自带的 Shiki
如 Astro 官方配置文档 所示
直接将 syntaxHighlight 设置为 false 即可。
export default defineConfig({
markdown: {
syntaxHighlight: 'shiki',
syntaxHighlight: false
// 其他配置略
}
})引入 rehype-mermaid
安装依赖
pnpm add rehype-mermaid
pnpm add playwright # rehype-mermaid 依赖
pnpx playwright-core install --with-deps chromium # 由于 mermaid 需要由服务器渲染为 SVG,所以需要一个无头浏览器环境接下来就是引入 rehype-mermaid 插件并配置暗黑模式, 然后再将 Shiki 插件引入渲染剩余的代码块。
import { defineConfig } from "astro/config";
import { rehypeShiki } from "@astrojs/markdown-remark";
import rehypeMermaid from "rehype-mermaid";
export default defineConfig({
markdown: {
rehypePlugins: [
// 脑图,流程图等,放在 Shiki 前加载
[
rehypeMermaid,
{
strategy: 'img-svg',
dark: true,
}
],
rehypeShiki,
],
syntaxHighlight: false
// 其他配置略
}
})样式
暗黑模式
首先,第一件事当然是观察 Mermaid 生成的 HTML 结构:
<picture>
<source height="521" id="mermaid-dark-0" media="(prefers-color-scheme: dark)" srcset="" width="1118.40625">
<img alt="" height="521" id="mermaid-0" src="" width="1118.40625">
</picture>可以看到,Mermaid 会生成两张图片,默认亮色会放在 <img> 标签中,暗色则放在 <source> 标签中。
众所周知,<source> 标签的渲染是完全由浏览器决定的,浏览器发出媒体查询,如果能够命中上述的 (prefers-color-scheme: dark),则会加载 <source> 中的图片,否则加载 <img> 中的图片。
如果你的网站只有随着系统切换的暗黑模式,那么 Mermaid 的暗黑模式就足够了。但是如果你的网站提供了一个手动切换配色的按钮,那么就需要一些 JS 来介入解决了。
诶你可能想问,这会儿你怎么又想着开始引入 JS 了,不是说不引入额外的 JS 吗?
噢亲爱的,你的切换主题按钮是如何实现的呢?那么直接在这个事件中切换 Mermaid 的图片不就好了吗?
type Theme = "light" | "dark" | "system";
// 更新 Mermaid 主题
function updateMermaidMedia(theme: Theme): void {
const mediaMap = {
system: '(prefers-color-scheme: dark)',
dark: 'all',
light: 'none'
};
document.querySelectorAll('[id^="mermaid-dark"]').forEach(el =>
el.setAttribute('media', mediaMap[theme] || 'none')
);
}
updateMermaidMedia(theme); // 加入到按钮被点击后触发的事件中当点击按钮切换主题时,调用 updateMermaidMedia 函数,传入当前主题字符串即可,如果跟你的配置不太一样的话,可以手动修改 mediaMap 的映射。
样式杂项
按照我上文的配置,Mermaid 最终会渲染为 SVG,如果你的博客在 CSS 里设置了针对 SVG 的全局样式,记得排除掉 Mermaid 的 SVG。
svg:not([id^="mermaid"]) {
/*Do your things*/
}此外,还有一个问题是 Mermaid 的图片宽度比父容器宽度小的话,会导致图片居左而不是居中,这个问题也需要通过 CSS 解决。
picture {
@apply flex justify-center;
}