許多的技術文件和部落格文章都使用 Markdown 格式來撰寫,不過你有沒有想過,這些Markdown是如何被轉換成 HTML的,顯示在這些技術文件和部落格上的?
常見的套件有 markdown-it、marked 等等,這些套件都可以將 Markdown 轉換成 HTML,而我打算要紹介的套件是 remark 和 rehype。

兩個都屬於 unified 生態系統的一部分,這個生態系統提供了一系列操作AST的工具和插件,讓我們可以輕鬆撰寫相關的plugin進行操作。
有了這幾個工具,我們可以輕鬆地將 Markdown 轉換成 HTML,並且在這個過程中使用各種插件來擴展功能。
在 unified 生態系統中,處理 Markdown 到 HTML 的過程通常是這樣的:
remark 解析 Markdown 文本,將其轉換為 AST,我們稱這個過程為parse,並稱這如此功能的plugin為parser。remark 插件來處理 AST,例如添加前置資料、支援 GFM、數學公式等功能。transform,並稱這如此功能的plugin為transformer。rehype 插件來處理 AST。在 unified 生態系統中,我們可以使用 unified 函式來創建一個處理管道,然後使用 use 方法來添加插件。
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkGfm from "remark-gfm";
import rehypeStringify from "rehype-stringify";
import rehypeHighlight from "rehype-highlight";
import rehypeKatex from "rehype-katex";
import remarkMath from "remark-math";
import remarkFrontmatter from "remark-frontmatter";
import remarkRehype from "remark-rehype";
const processor = unified()
.use(remarkParse) // 解析 Markdown (String -> Markdown AST)
.use(remarkGfm) // 支援 GFM (GitHub Flavored Markdown)
.use(remarkMath) // 支援數學公式
.use(remarkFrontmatter, ["yaml"]) // 支援 YAML 前置資料
.use(remarkRehype) // 將 Markdown AST 轉換成 HTML AST
.use(rehypeKatex) // 將數學公式轉換成 HTML
.use(rehypeHighlight) // 將程式碼塊轉換成 HTML
.use(rehypeStringify); // 將 HTML AST 轉換成 HTML 字串`
const markdown = `
# Hello World
\`\`\`js
console.log('Hello World');
\`\`\`
$$
E = mc^2
$$
`;
const html = await processor.process(markdown);
console.log(html);
這段程式碼展示了如何使用 unified 的套件和remark/rehype 的plugin來處理 Markdown 文本,並在最後將生成的 HTML 輸出到控制台。
這是一個最簡單的例子,許多處理Markdown的套件,都會提供remark/rehype 的plugin來擴展功能,讓我們可以輕鬆地處理 Markdown 文本,舉個例子,Next.js的 mdx套件,就提供了remarkPlugins/rehypePlugins 的Options讓使用者填入。
AST(抽象語法樹)是一種樹狀結構,用於表示程式碼的語法結構。它將程式碼分解為更小的部分,並以樹狀結構的形式表示,使得我們可以更容易地分析和處理程式碼。 在 unified 生態系統中,AST 是一個 JavaScript Object,包含了各種節點,每個節點代表程式碼中的一個元素,例如標題、段落、程式碼塊等。
以下是一些常見的 remark AST 節點:
詳細的節點類型和結構可以參考 remark 的官方網站, 以及 unified 的Github.
以下是一些常見的 rehype AST 節點:
詳細的節點類型和結構可以參考 rehype的Github, 以及 unified 的Github
AST 正如前面說的,是一個樹狀結構,我們可以選擇自己寫遞迴來操作合改變這些樹狀結構,但是不方便。 實際上unified 生態系統提供了一系列的 函式庫 來撰寫插件,讓我們可以輕鬆地操作 AST,這邊簡單介紹幾個我自己寫自己plugin用到的函式庫。
以下是一個簡單的 remark 插件範例,這個插件會將所有的標題節點轉換成大寫:
import { visit } from 'unist-util-visit';
import type { Plugin } from 'unified';
const remarkUppercaseHeadings: Plugin = () => {
return (root) => {
visit(root, 'heading', (node) => {
node.children = node.children.map((child) => {
if (child.type === 'text') {
child.value = child.value.toUpperCase();
}
return child;
});
});
};
};
export default remarkUppercaseHeadings;
這個插件使用了 unist-util-visit 函式庫來遍歷 AST,並將所有的標題節點轉換成大寫。
這個插件可以直接使用在 unified 的處理管道中:
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkUppercaseHeadings from "./remark-uppercase-headings";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
import { inspect } from "unist-util-inspect";
const processor = unified()
.use(remarkParse) // 解析 Markdown (String -> Markdown AST)
.use(remarkUppercaseHeadings) // 使用自定義的插件
.use(remarkRehype); // 將 Markdown AST 轉換成 HTML AST
const markdown = `
# Hello World
\`\`\`js
console.log('Hello World');
\`\`\`
`;
const html = await processor.process(markdown);
// 使用 unist-util-inspect 來查看 AST 的結構和內容
console.log(inspect(html));
你會在控制台看到所有的標題節點都被轉換成大寫了。
實際上,這還只是個簡單的範例,我們還可以透過 unist-util-visit 來修改父節點、添加新的節點、刪除節點等等,這些操作都可以通過訪問 AST 節點來實現。