我在 Astro 行內程式碼的色碼旁加上了色塊 — 為什麼選 rehype 外掛而不是 Swatch 元件?
這是一個用在 Astro MDX 上的 rehype 外掛,會在每個寫成行內程式碼的十六進位色碼前畫出色塊。我選了 rehype 外掛而不是 Swatch 元件,這樣一來既有文章都不用修改。

本頁目錄
引言
我在重讀我之前寫的 logo 文章時,看到其中一段主張網站強調色 #1d4ed8 和印章顏色 #d8452a 互相衝突。我這才發現,這段文字要讀者比較兩個根本看不到的顏色。VS Code 處理 CSS 等檔案時也會遇到同一個問題,它的做法是在每個十六進位色碼前面畫一個該顏色的小方塊。
我用 rehype 外掛在這個部落格上實現了同樣的效果。現在所有既有文章在三種語言版本裡的色碼前面都有了色塊,讓讀者一眼就能看出色碼代表的顏色。
讓每個行內色碼都帶有色塊
VS Code 把這個功能稱為 color preview,可以用 editor.colorDecorators 設定開啟或關閉。下圖是這個功能套用在這個部落格圖表用的 token 上的樣子:

src/styles/global.css 的 --fig-* token 裡,於每個色碼前顯示 color preview。部落格這邊的實作是 src/lib/rehype-color-swatch.ts 裡的 rehypeColorSwatch。Astro 透過 unified 這套語法樹處理工具把 Markdown 轉成頁面,rehype 是其中處理 HTML 的階段。rehype 外掛是一個函式,會以 hast(HTML 語法樹)的形式接收頁面,並直接修改這棵樹。
外掛遇到行內 <code> 時,只要整段文字都是十六進位色碼,就會在文字前插入一個空的 <span>,再把顏色當成自訂屬性交給 CSS。以 #1d4ed8 為例,建置後的 HTML 如下:
<code> <span class="color-swatch" aria-hidden="true" style="--swatch:#1d4ed8"></span> #1d4ed8</code>這個 span 裡沒有文字,又帶有 aria-hidden。因此複製程式碼時只會複製到 #1d4ed8,螢幕閱讀器也只會念出色碼。方塊由一條 CSS 規則畫出,尺寸以 em 指定,所以會像字形一樣跟著程式碼的字體大小縮放:
.color-swatch { display: inline-block; width: 0.8em; height: 0.8em; margin-inline-end: 0.35em; vertical-align: -0.05em; background: var(--swatch); border: 1px solid var(--text-3); border-radius: 2px;}註冊外掛只需要加一行 import,再在 Markdown 處理器的外掛陣列裡加一個項目:
import { rehypeColorSwatch } from "./src/lib/rehype-color-swatch.ts";// …markdown: { processor: unified({ rehypePlugins: [rehypeTableScroll, rehypeLinkIcons, rehypeColorSwatch] }),},@astrojs/mdx 預設會沿用這份 Markdown 設定(除非設成 extendMarkdownConfig: false),所以這一個項目就涵蓋了所有 .mdx 文章。日文版和繁體中文版的檔案裡,也有和英文版相同的反引號。因此這兩個版本也得到了相同的色塊。
為什麼選 rehype 外掛而不是 <Swatch> 元件?
元件只會在寫了元件的地方畫出色塊,外掛則會在任何放進反引號的色碼旁畫出色塊。這個部落格一直都是把色碼寫在反引號裡。因此只要加上這個外掛,就不需要再修改已經發布的文章。
| rehype 外掛 | <Swatch> 元件 | |
|---|---|---|
| 作者要寫的內容 | 跟以前一樣寫 #1d4ed8 | <Swatch color="#1d4ed8" />,再加一行 import |
| 既有文章 | 下一次建置就有色塊 | 每個色碼都要手動改寫 |
| 日文版和中文版 | 同樣的反引號,同樣的色塊 | 每個檔案裡的色碼都要再改寫一次 |
| 之後用一般反引號寫的色碼 | 會有色塊 | 只會顯示成一般的程式碼 |
如果用元件,一般的反引號會繼續顯示成一般的程式碼。這樣一來,往後任何一篇文章有沒有色塊,都取決於作者記不記得要用元件。用 AI 寫文章時,也要確保 AI 用元件而不是反引號來寫色碼,但就算定了規則,AI 也不一定會嚴格遵守。
外掛會檢查樹狀結構的哪些地方?
外掛會走訪 <pre> 以外的所有元素,檢查走到的每個 <code> 的文字,也會走進 MDX 元件的內部。
- 只處理行內程式碼。走訪不會進入
<pre>。這個部落格的程式碼區塊由 Expressive Code 這個外掛算繪,區塊裡的內容都由它處理。因此程式碼區塊裡的色碼會維持原樣。 - 只處理整段文字都是色碼的情況。符合條件的
<code>只能有一個子節點,而且是符合十六進位色碼的文字節點。因此color: #fff不會有色塊。在一段較長的程式碼裡,外掛分不出#fade是顏色還是 CSS 的 id 選擇器,因為它不像 VS Code 那樣會解析程式碼的語言。 - 會走進 MDX 元件。
<Callout>這類元件在樹狀結構中不是element節點,走訪時必須指名它的型別才能進入。
元件裡面的 Markdown 仍然是一般的 hast,但如果走訪只往 element 節點裡走,就會停在元件的邊界上。結果就是提示框裡的色碼沒有色塊,段落裡同樣的色碼卻有。
走訪用手寫的遞迴實作,下面標示出來的分支負責走進元件:
// 三位數和四位數的寫法必須含有英文字母,所以 `#447` 這種 issue 編號不會被加上色塊const HEX = /^#(?:[0-9a-f]{6}|[0-9a-f]{8}|(?=[0-9]*[a-f])[0-9a-f]{3,4})$/i;
function addSwatches(node: HastNode): void { for (const child of node.children ?? []) { if (child.type === "mdxJsxFlowElement" || child.type === "mdxJsxTextElement") { addSwatches(child); continue; } if (child.type !== "element") continue; if (child.tagName === "pre") continue;
if (child.tagName === "code") { const color = colorOf(child); if (color) { (child.children ??= []).unshift({ /* 建立 class="color-swatch" 的 span 節點,程式碼省略 */ }); } continue; }
addSwatches(child); }}在深色和淺色背景上都看得見的外框
這個部落格原本就有一個 --border token,用來畫細分隔線,行內程式碼的外框用的也是它。色塊位在行內程式碼的底色上,但這個底色和 #0b1120 色塊在深色模式下的對比度只有 1.06:1,和 #ffffff 色塊在淺色模式下只有 1.05:1。
遇到這類接近頁面背景的顏色時,填色等於什麼都沒畫,看得見的只有外框,而 --border 外框和這兩個色塊的對比度也只有 1.28:1 和 1.23:1。
所以色塊的外框我改用 --text-3,也就是這個部落格用在次要文字上的灰色。這樣一來,就算顏色很接近頁面背景,色塊也還是會顯示成一個有外框的方塊。下表列出兩個 token 的外框和這兩個色塊之間的對比度:
| 外框 token | 深色模式下對 #0b1120 | 淺色模式下對 #ffffff |
|---|---|---|
--border | 1.28:1 | 1.23:1 |
--text-3 | 3.96:1 | 4.76:1 |
根據網頁無障礙標準 WCAG 2.2 要求,要讓讀者看得到圖形元素,其對比度至少要達到 3:1。--text-3 在兩種主題下都達標,--border 則兩種都沒有達到。

--border 時,與頁面背景同色的色塊幾乎看不見。
--text-3 時,兩個色塊在兩種主題下都看得出是方塊。顏色透過 style 屬性傳遞
文章中可能使用任何十六進位色碼,而我不可能為每個顏色都準備一個 class。所以外掛把顏色寫進 style="--swatch: #0b1120",CSS 規則再用 background: var(--swatch) 取出 #0b1120 並套用這個顏色。
行內的 style 屬性受頁面的 Content Security Policy(CSP)管制。CSP 是伺服器透過回應標頭告訴瀏覽器,頁面可以載入和執行哪些內容的機制。這個部落格送出的 CSP 是 style-src 'self'; style-src-attr 'unsafe-inline'。
Shiki 是 Expressive Code 內部使用的語法標示工具,它會把顏色寫在程式碼區塊的 style 屬性上。所以 style-src-attr 'unsafe-inline' 原本就在政策裡,這個決定我在之前寫的 CSP 文章裡說明過。因此這次新增的外掛完全不需要修改這份政策。
如果一個網站沒有 style-src-attr,style-src 裡也沒有 'unsafe-inline',瀏覽器就會擋下行內的 style 屬性,所有色塊都只會顯示成一個空的外框方塊。
總結
現在這個部落格裡,只要把十六進位色碼寫成行內程式碼,前面就會顯示該顏色的色塊。色塊是由 rehype 外掛畫出來的,而不是元件。所以既有文章不需要任何修改,就都有了色塊。具體的做法如下:
- 把外掛註冊在
markdown.processor,@astrojs/mdx會沿用它。 - 只處理整段文字都是色碼的行內
<code>。 - 略過
<pre>,並走進mdxJsxFlowElement和mdxJsxTextElement的內部。 - 色塊外框選用的 token,在深色和淺色背景上的對比度都要達到 3:1。
- 在有 CSP 的網站上,若要把顏色放在
style的自訂屬性裡傳遞,需要讓style-src-attr允許行內屬性。

