Markdown 的代码高亮
每次尝试新兴事物时,总会遇到一些坑点。但这次踩坑的路线与以往不同之处在于,我在解决自己需求时,利用了 AI 帮我搜索。我已经记不清是第几次被 AI 误导了。虽然部分 AI 会自己抓取网页,但很多内容其实已经过时了。这是因为新技术层出不穷,有时候稍不注意,就会翻到几年前的技術文档或博客,看完后才发现相关内容早就被废弃了。
参考链接
- 语法高亮文档 - Astro
- Markdown 插件 - Astro
- unifiedjs - 官方网站
- astro-expressive-code 官方文档
- remark.js - 官方网站
- rehype.js - GitHub
前言
本文的原始内容,以及我的其他博客内容,都是用 Markdown 编写的。因为我从事程序开发,文章中一定会涉及到代码内容。之前我的博客采用 Vitepress ,当前是 Astro。
它们的基础都是 Vite,但 Astro 与 Vitepress 定位不同。Astro 比 Vitepress 的自由度更高。因此,像 Vitepress 中自带的代码复制按钮和独特的样式设置,在 Astro 中都需要自己加入插件来实现。
Astro 与 Markdown
由于 Markdown 生成 HTML 在多种编程语言中都存在,特此说明:本文以 JavaScript 语言生态为例。
了解 Astro 背后的动机,能让你在想要拓展 Markdown 功能时更快地找到方向。本文产生的源头,正是关于 Markdown 中编程代码的语法高亮和交互需求。
Markdown 解析
此部分介绍 Astro 框架如何处理(解析)Markdown 原始内容。
目前,社区对 Markdown 的解析有多种形式:有的是直接对 Markdown 进行转译,生成 HTML;有的则将 Markdown 解析为 Tokens;还有的将 Markdown 转换为抽象语法树(AST)。
Google AI 给出了以下流行的 Markdown 解析库:
- marked: 以速度著称,是目前最流行、轻量级的解析器之一,支持浏览器、服务器和命令行。
- markdown-it:解析极度精准(100% 符合 CommonMark 标准),且扩展插件极丰富,常作为 VS Code 等编辑器的底层。
- remark:基于插件系统的生态。它不只是解析,还支持转换为抽象语法树(AST),适合需要对 Markdown 进行深度结构化处理的场景。
Astro 的 Markdown 解析能力是基于 unifiedjs 下的 remark 库的。也就是说,Astro 会将 Markdown 内容解析为抽象语法树。下文将介绍 Astro 如何处理 AST。
[!warning]
remark是一个另外一个主题,这里作者能力有限,此处不再深究。
AST 到 HTML
上文仅介绍了 Markdown 的解析。由于 Astro 选择了 remark 作为解析器,因此还需要基于 unifiedjs 下的 rehype,来实现从 Markdown 的 AST 转化为 HTML。
remark + rehype 提供了一个类似 Markdown 处理的流水线。即从 Markdown 原始文档,到抽象语法树,再将抽象语法树输出为 HTML。
--- title: astro 的 markdown处理 --- flowchart LR A[Markdown 原始内容] --> B[remark 解析] B --> C[mdast 抽象语法树] C --> D[rehype 转换] D --> E[hast HTML 语法树] E --> F[最终 HTML 输出]
Markdown 中的代码高亮
从 Astro 的 Markdown 处理流程来看,对代码进行高亮一定是在 AST 到 HTML 阶段,也就是在 rehype 插件处理阶段进行。
为什么 Markdown 的库不能一次性处理完所有的事情,还要再加入一个 rehype 插件?这不是让框架的学习和使用成本上升了吗?
其实不然。打个比方,Markdown 中包含代码块,意味着代码块中可能包含其他编程语言。将 Markdown 本身作为一种语言,让其他语言有专门负责的库去处理他们的语法,是一个非常明智的选择。因为 markdown 不可能处理所有语言语法,特别是在不同语言的处理方法和原则还不一样的情况下。
因此,对于同样在 Markdown 中的语言,将 他们 的处理和他们负责的库 ( 此处标识代码高亮 ) 分开来看,就好理解多了。
本人在初次接触时,对这部分内容十分不理解,为什么一个 Markdown 会搞得这么复杂?因为潜意识里想把代码高亮的工作全交给 Markdown 处理。殊不知,无论是 Markdown、JavaScript、Python、Golang 还是 Java,都是一门独立的语言,不同的语言风格都是不同的。因此,当 Markdown 处理自身以外的内容时,最好的办法就是利用好已有的生态。
代码高亮库
- Shiki: 基于 TextMate 语法,在服务端处理,对 SEO 友好。
- Prism.js: 基于正则匹配,在客户端运行。
- Highlight.js: 基于正则匹配,在客户端运行。
Astro 内置的代码高亮
理解完 Markdown 和语法高亮为什么要分开处理后,再回归 Astro 的代码高亮中。
从 Astro 出发,它内置了多个代码高亮库:
- Shiki
- Prism
Shiki 是 Astro 内置的默认高亮方案。原因也很直接:Shiki 讲究零运行时,这与 Astro 的静态内容优先理念非常吻合,都是为了减轻客户端的渲染压力,让服务器负责主要内容。
同时,Shiki 本身也是一个独立的库,其文档也是需要去了解的。也有些库是基于 Shiki 的。本站就采用了其拓展库 astro-expressive-code。
Prism.js 作为一个运行时的代码高亮库,本文不采用。毕竟我对客户端运行时也不是很喜欢了。
Astro 社区高亮方案
基于 Astro 的选择,在满足个人需求的同时,尽可能贴近 Astro 官方的理念。基于 Shiki 衍生出的 rehype-pretty-code 和 astro-expressive-code 都是可以选择的库。它们都是基于 Shiki 的。
由于现阶段我不是很愿意花非常多的时间去了解 Shiki 和 unifiedjs 庞大的生态,我的站点中采用了 astro-expressive-code,由此简单实现了我的需求。
结语
综合上文, markdown和markdown中高亮代码的处理流程如下
---
title: markdown 在 astro 的处理流程
---
graph TD
A[Markdown 源文件] --> B[Remark 阶段: 解析 Markdown]
B --> C{Remark 插件}
C -->|转换| D[Rehype 阶段: HTML 语法树]
subgraph Shiki_Processing [Shiki 高亮核心]
D --> E[识别 pre/code 标签]
E --> F[Tokenize 词法分析]
F --> G[注入内联样式/CSS]
end
G --> H[Rehype 插件: 其他 HTML 修改]
H --> I[Stringify: 输出最终 HTML]
I --> J[Astro 页面渲染]
style Shiki_Processing fill:#f9f,stroke:#333,stroke-width:2px
AI 正当时,Markdown 作为 AI 唯一指定的通用语言,其生态得到了一定程度的发展。如果将 Markdown 作为与 AI 交流的通用语言,实际上对于开发者还是挺不错的。至少对我来说,我对 Markdown 所具有的生态和渲染魔法有一股莫名的偏爱。
从接触 Astro 到现在探索 Markdown 生态,有了一些小收获, 让我的markdown生态更加喜欢。