Skip to main content

ZSL

Hugo 語法上色 Syntax Highlighter

Published:
Updated:

這是把語法上色從 Hugo 內建的 Chroma 改成 prism.js/highlight.js 又改成 shiki 的心得,網路上你哪裡能找到全部試過一輪的心得呢,想改的原因不外乎就是顏色醜,辨識度低,不好看。

Syntax highlighter 選擇就三大要點,第一語法解析正確,第二執行速度和方式,第三顏色主題選擇,Chroma 是伺服器端就已經幫你渲染好的因此就是最快在 Hugo 也是開箱即用,但是 Chroma 語法解析稀巴爛完全亂標一通,顏色錯誤看了就很不爽,而且主題也沒幾個可以選。

說完 Chroma 的特色之後來說說其他幾個選項,Prism.js 和 Highlight.js 都是在客戶端執行時在瀏覽器載入 JS 渲染外觀,因此效能不可避免的差,現在最好的選擇就是選用 Shiki syntax highlighter,同時支援客戶端和伺服器端渲染,要方便還是效能任君選擇,而且最重要的是他採用和 VS Code 相同的 lexer (詞法分析),因此語法判斷正確性保證是最高品質的。

最古老版本寫的舊文章,文字本身寫的很爛,而 JS 方案也不推薦,別看了

Chroma

這是 Hugo 內建的語法上色方式,以 Blowfish 主題而言,可以在 assets/css/components/chroma.css 找到。保持一勞永逸的想法(雖然真的一勞永逸的次數好像滿少的)先去查哪個 highlighter 好,2018 2019年那些文章大概都說 Prism.js,說維護的比較勤 contributor 比較多,那就先選 Prism.js 好了。

Prism.js 和 Highlight.js

Prism.js 裝好沒半小時去官方 Github 看發現上次更新已經是 2022,既然沒在維護就不用了,於是果斷換成 Highlight.js,也很輕鬆兩行 <link> <script> 就搞定,只是痛苦這裡才開始。首先,我已經知道 PageSpeed Insights 這個邪惡的東西,裝完之後手賤去測試果然慢了不少,因為他是用戶載入頁面後即時渲染的;再來是暗色模式支援,還自己寫了一堆 javascript 搞定亮暗主題轉換問題。

加個 highlight 為什麼要這麼多 JS?除了寫太爛打掉重練,還有客製化會導致其他依賴原有 HTML 架構的功能失效,否則單純只加上 Highlight.js 確實是像教學1一樣複製貼上就結束了,但是改成 Highlight.js 後缺失的主題切換和複製按鈕都要重新寫,關於 CSP referrerpolicy, defer/async, crossorigin, integrity 的設定教學也沒講到,查到 CSP 之後又用 Cloudflare Workers 寫了一個修改 HTTP headers 的程式:

// Cloudflare Workers
const cdns = `
  https://cdnjs.cloudflare.com
  https://cdn.jsdelivr.net
  https://fonts.googleapis.com
  https://fonts.gstatic.com
`.trim().replace(/\n\s+/g, ' '); 

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
  let response = await fetch(request)

  let newHeaders = new Headers(response.headers)

  newHeaders.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload')
  newHeaders.set('X-Content-Type-Options', 'nosniff')
  newHeaders.set('X-XSS-Protection', '1; mode=block')
  newHeaders.set('Content-Security-Policy', `
    object-src 'none';
    default-src 'self';
    manifest-src 'self' https://*.zsl0621.cc;
    connect-src 'self' ${cdns} https://www.google-analytics.com;
    font-src 'self' ${cdns};
    img-src 'self' https://*.zsl0621.cc;
    script-src 'self' https://*.zsl0621.cc ${cdns} https://www.googletagmanager.com https://analytics.google.com  https://static.cloudflareinsights.com 'unsafe-inline';
    style-src 'self' https://*.zsl0621.cc ${cdns} 'unsafe-inline'
  `.replace(/\n/g, ' ').trim())
  newHeaders.set('X-Frame-Options', 'SAMEORIGIN')
  newHeaders.set('Referrer-Policy', 'strict-origin')
  newHeaders.set('Permissions-Policy', 'geolocation=(self), microphone=(), camera=()')
  newHeaders.set('Cache-Control', 'public, max-age=31536000')

  const origin = request.headers.get('Origin')
  if (origin && origin.match(/^https:\/\/.*zsl0621\.cc$/)) {
    newHeaders.set('Access-Control-Allow-Origin', origin)
  } else {
    newHeaders.set('Access-Control-Allow-Origin', '*')
  }

  return new Response(response.body, {
    status: response.status,
    statusText: response.statusText,
    headers: newHeaders
  })
}

越搞越多跟想像的三行優雅解決完全不一樣...都破百行了,最重要的是原本用好的超過100分經過字體和高亮 CDN 以及我外行人寫的 javascript,速度直接噴到剩下50分:

hljs-mobile hljs-desktop

剛部屬完 Highlight.js 馬上測,手機版有時候甚至還跑不到50分。

Shiki

我辛苦弄這麼久的結果雖然是好看了但是分數有夠爛,那我之前的努力算什麼,就在我覺得好像沒救的時候看到 Eallion 寫的 在 Hugo 中使用 Shiki,插入也有夠簡單而且內建亮暗主題切換,重點是純靜態,拿他的網頁去跑分即使程式碼數量比我多的也是輕鬆跑到99, 100,好啊心態沒了,馬上改全刪,於是現在的跑分成績:

shiki-mobile shiki-desktop

完美,回來了。

更新

eallion 使用 rehype 工具執行 shiki,執行速度非常慢且不支援編輯時預覽,因此我寫了一個腳本優化,請見在 Hugo 中使用 Shiki 並且加快執行速度超過百倍

後記

eallion 寫的 Shiki 教學就是我最喜歡的那種,有介紹原因,直接教學,沒有廢話,沒有拖泥帶水,清楚了當。

最後,你說70分和100分體感有差嗎?完全沒差,但是我比較爽。