使用 Shiki.js 對網頁上的程式碼進行著色
前陣子突然又心血來潮,將自己部落格的程式碼著色工具從 Highlight.js 換成 Shiki.js。
雖然 Highlight.js 使用上並沒有什麼問題,我甚至還使用它提供的 API,幫自己寫了 Blade 與 HCL 的程式碼著色套件。Highlight.js 比較可惜的點是目前幫忙維護的人並不多,而且 Shiki.js 支援的語法、主題、還有功能都比 Highlight.js 多得多,在經過不怎麼激烈的思想鬥爭之後,就決定改為使用 Shiki.js 了。
Shiki.js 的作者是 Vue Core Team 的 Anthony Fu 大大,背後使用的是與 VS Code 相同的程式碼著色引擎 TextMate,所以著色效果非常好,而且還支援明暗主題切換。
簡單介紹 Shiki.js 的用法
Highlight.js 本身提供相當方便的函式,讓你可以直接對頁面上的包含程式碼的 <pre> 元素進行著色:
hljs.highlightAll()hljs.highlightElement(element)
但 Shiki.js 並沒有提供類似的 API,只有一個簡單的 codeToHtml。傳一組字串進去,並指定語言和主題,它就會回傳著色後的 HTML 字串。
import { codeToHtml } from 'shiki'
const code = 'const a = 1' // input code
const html = await codeToHtml(code, {
lang: 'typescript',
theme: 'one-dark-pro',
})
console.log(html) // highlighted html string渲染後的 html 字串如下:
<pre class="shiki one-dark-pro shiki-highlighted" style="background-color:#282c34;color:#abb2bf" tabindex="0">
<code>
<span class="line">
<span style="color:#C678DD">const</span>
<span style="color:#E5C07B"> a</span>
<span style="color:#56B6C2"> =</span>
<span style="color:#D19A66"> 1</span>
</span>
</code>
</pre>可以看到 Shiki.js 是使用 Inline CSS 對程式碼進行著色,所以不需要再額外引入主題的 CSS 檔案。
了解了該怎麼使用 Shiki.js 後,就可以開始來寫扣了!
開始動工
使用 NPM 安裝 Shiki.js 套件。
npm install -D shiki我們需要建立一個 Highlighter 實體。
import { createHighlighter, type Highlighter } from 'shiki';
let highlighter: Highlighter | null = null;
async function getHighlighter(): Promise<Highlighter> {
if (!highlighter) {
highlighter = await createHighlighter({
// 指定會使用到的程式語言
langs: ['javascript', 'typescript', 'php', ...languages],
// 引入主題
themes: ['one-dark-pro'],
});
}
return highlighter;
}
// ...Shiki.js 的 Best Performance Practices 中有提到,
Highlighter實體的建立是很耗資源的,所以建立後就應該要留著,並在之後的著色操作中反覆使用(單例模式)。
寫一個 highlightElement 函式,用來對包含程式碼的 <pre> 元素進行著色。
// ...
async function highlightElement(
preElement: HTMLPreElement,
highlighter: Highlighter,
) {
// 避免重複著色
if (preElement.classList.contains('shiki-highlighted')) {
return;
}
// 取得 <code> 元素
const codeElement = preElement.querySelector('code');
if (!codeElement) {
return;
}
// 取得 <code> 元素上標注的語言類型
const langClass = Array.from(codeElement.classList).find((c) =>
c.startsWith('language-'),
);
const lang = langClass ? langClass.replace('language-', '') : 'text';
// 取得程式碼
const code = codeElement.innerText;
try {
// 使用 highlighter 對程式碼進行著色
const html = highlighter.codeToHtml(code, {
lang,
theme: 'one-dark-pro',
});
const template = document.createElement('template');
template.innerHTML = html.trim();
const pre = template.content.firstChild as HTMLElement;
// 將原本的 <pre> 元素替換成著色後的程式碼 <pre> 元素
preElement.replaceWith(pre);
// 添加標記,避免重複著色
pre.classList.add('shiki-highlighted');
} catch (e) {
console.warn(`Failed to highlight language: ${lang}`, e);
// Fallback or ignore
}
}利用剛剛寫好的 highlightElement 函式,寫一個 highlightAllInElement 函式,用來對指定元素下的所有包含程式碼的 <pre> 元素進行著色。
async function highlightAllInElement(htmlElement: HTMLElement): Promise<void> {
const highlighter = await getHighlighter();
let preElements = htmlElement.querySelectorAll(
'pre:not(.shiki-highlighted)',
) as NodeListOf<HTMLPreElement>;
for (const preElement of preElements) {
await highlightElement(preElement, highlighter);
}
}寫好了就可以將 highlightAllInElement 函式掛到 window 上,方便之後使用。
declare global {
interface Window {
highlightAllInElement: (element: HTMLElement) => Promise<void>;
}
}
window.highlightAllInElement = highlightAllInElement;<script>
window.highlightAllInElement(document.body);
</script>根據網頁的明亮/暗黑模式來切換 Shiki.js 的主題
Shiki.js 支援明亮與暗黑主題的切換,你可以根據據網頁當前的模式來使用不同的著色主題。
在 createHighlighter 中,我們可以一次引入兩種主題。
highlighter = await createHighlighter({
langs: ['javascript', 'typescript', 'php', ...languages],
// 引入多種主題
themes: ['one-light', 'one-dark-pro'],
});在著色前設定明亮與暗黑模式下分別要使用的主題。
const html = highlighter.codeToHtml(code, {
lang,
// 設定在明亮與暗黑模式下要使用的主題
themes: {
light: 'one-light',
dark: 'one-dark-pro',
},
});如果你跟我一樣,是依據 <html> 的 data-theme 屬性來切換網頁模式 ,接下來只要在網頁上加入下面這段 CSS 就可以了。
/* 根據 <html> 元素上的 class name 是否包含 dark 來切換主題 */
html[data-theme='dark'] .shiki,
html[data-theme='dark'] .shiki span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
}
pre.shiki {
padding: 1rem !important;
border-radius: 0.75rem !important;
}
pre.shiki code {
display: block !important;
font-family: var(--font-jetbrains-mono), sans-serif !important;
font-size: var(--text-lg) !important;
line-height: var(--text-lg--line-height) !important;
font-weight: 600 !important;
overflow-x: auto !important;
}加上行數編號
Shiki.js 本身並沒有行數編號的功能,但是在 Shiki 的某串 Issue 討論串中,有一位大大提供了一段神秘的 CSS,讓你可以在完全不需要 JavaScript 的情況下,幫程式碼加上行數編號。
pre.shiki code {
counter-reset: step; /* 建立一個名為 "step" 的計數器,預設歸零 */
counter-increment: step 0; /* 在這個容器層級不增加數值 (加 0) */
}
code .line::before {
content: counter(step); /* 讀取並顯示計數器目前的數字 */
counter-increment: step; /* 讓計數器 +1 */
width: 1rem; /* 設定行號欄位的固定寬度 */
margin-right: 1.5rem; /* 設定行號與程式碼之間的距離 */
display: inline-block; /* 讓行號變成區塊,才能設定寬度 */
text-align: right; /* 讓數字靠右對齊 */
color: rgba(115,138,148,.4); /* 設定行號顏色 (通常較淡) */
}原來 CSS 還有計數器這種功能?真是太神奇了。
使用 Transformers 突顯特定行數
Shiki.js 支援 Transformers 功能,讓你能更彈性的調整產生的 HTML 內容,例如在程式碼區塊中突顯特定行數。
官方已提供多款內建的 Transformers 供開發者直接呼叫,我們可以直接使用。
npm i -D @shikijs/transformers接下來簡單示範一下如何使用 Transforms 來突顯特定行數。從 Transforms 套件中引入 transformerNotationHighlight 後,就可以在 codeToHtml 中使用這個 Transforms。
import { transformerNotationHighlight } from '@shikijs/transformers';
// ...
const html = highlighter.codeToHtml(code, {
lang,
themes: {
// ...
},
colorReplacements: {
// ...
},
transformers: [
transformerNotationHighlight() // [!code highlight]
]
});突顯行數的方式很簡單,只要在註解中標註 [!code highlight] 即可,有兩種標註方式。
// 第一種方式,把註解加在該行後面
console.log('hello'); // [\!code highlight]
// 第二種方式,把註解加在該行上面一行,後面可以使用數字指示要突顯幾行
// [\!code highlight:2]
console.log('這一行會被突顯');
console.log('這一行也會突顯');
console.log('這一行不會被突顯');需要注意的是,Transformers 預設不會有任何樣式,它只會在 HTML 中幫你加上 highlighted 的 Class Name。
<span class="line highlighted">
<!-- ... -->
</span>因此我們需要自己加上 CSS 樣式。下面是我部落格的版本。
pre.shiki {
padding: 0 !important;
border-radius: 0.75rem !important;
}
pre.shiki code {
display: block !important;
width: fit-content !important;
min-width: 100% !important;
padding: 1rem !important;
/* ... */
}
pre.shiki code span.line.highlighted {
display: inline-block !important;
width: calc(100% + 2rem) !important;
margin: 0 -1rem !important;
padding: 0 1rem !important;
background-color: oklch(70.7% 0.022 261.325 / .2) !important;
}
pre.shiki code span.line.highlighted span {
background-color: transparent !important;
}有了樣式之後的效果如下:
// 第一種方式,把註解加在該行後面
console.log('hello'); // [!code highlight]
// 第二種方式,把註解加在該行上面一行,後面可以使用數字指示要突顯幾行
// [!code highlight:2]
console.log('這一行會被突顯');
console.log('這一行也會突顯');
console.log('這一行不會被突顯');除了突顯行數,官方的 Transforms 還提供突顯 Diff 刪減行數等多種功能,更多詳細內容可以參考文件。