Astro 修改(1) -- 增加 MarkDown Mermaid 支持


关于 Mermaid

因为图表和文档会耗费开发者宝贵的时间,并且很快就会过时。但是,没有图表或文档会破坏生产力并损害组织学习。

所以 Mermaid 应用而生,Mermaid 允许你使用文本和代码创建图表和可视化。它是一个基于 JavaScript 的图表绘制工具,可渲染 Markdown 启发的文本定义以动态创建和修改图表。

在 Astro GitHub 的 Issue 中,有人提出了 Astro 不支持 Mermaid 的问题 #4433
下面也有赛博菩萨贴出来了自己的解决方案,动态的引入 Mermaid JS 来解决

但是在 Astro 这种静态网站生成框架的背景下,在能不传给客户端 JS 的前提下绝对不应该传递给客户端 JS,所有的页面渲染都应该在服务端完成。
所以我决定在不在客户端引入额外 JS 的情况下,为 Astro 增加 Mermaid 支持。

理解 Astro 渲染流程

Mermaid 图表 亮色Mermaid 图表 暗色

上图是 Astro 渲染 MD 的简化流程,实际上还有许多其他的渲染器和包装器在起作用,但这里我只展示了最重要的一些。
此外,Remark 和 Rehype 都是基于 unified 架构的抽象语法树转换器,它们内部还有许多流程和插件,上图也没有展示,你可以去阅读 unified 官方 GitHub 中的说明

总体来说,一篇 MD 渲染为 HTML 的流程是:

  1. Astro 读取 MD 文件为字符串,剔除 Front Matter 并作为 Global Config。
  2. Astro 通过 Remark 将 MD 字符串转换为 mdast。这是一个针对 MD 的抽象语法树。
  3. Astro 通过 Remark-rehypemdast 转换为 hast。这是一个针对 HTML 的抽象语法树。
  4. 如果没有禁用 Shiki 的高亮,Astro 会将 hast 传给 rehype-shiki 插件,将代码块转换为高亮的 HTML。
  5. 运行配置文件中写的其他 rehype 插件
  6. 使用 rehype-stringifyhast 转为 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;
}

参考



评论加载中……