cho-men

cho-menは、世路庵メンバーがお届けする情報発信メディアです。

シンタックスハイライター「Shiki」で、美しいコードブロックを手に入れよう

フロントエンドエンジニア

小山 樹人

投稿日: 2024.07.16

  • フロントエンド

ソースコードのシンタックスハイライトはどのように対応していますか? 普段コードを書いていると、QiitaZennなどの技術記事プラットフォームや技術系のブログなどで、ハイライトされたコードを見ることは多いのではないでしょうか?

Shikiは、シンタックスハイライトのためのJavaScriptライブラリです。多くの主要プログラミング言語に対して、非常に正確で高速なシンタックスハイライトを提供します。名前の由来は日本語の「式」で、「スタイル」を意味しています。

WebフレームワークのAstroNuxtのコンテンツ管理モジュールであるNuxt Content、スライド作成ツールの Slidevなどでもシンタックスハイライトに利用されています。

本サイトのコードブロックも、すべてShikiを利用してハイライトしています!

Shikiの特徴

シンタックスハイライトのためのJavaScriptライブラリというと、代表的なものにPrismhighlight.jsがあります。Prismは、軽量で拡張性が高く、プラグインを利用して機能を追加できるといった特徴があります(現在は、v2への移行に向けて2022年頃から開発がストップしている状態のようです Roadmap for Prism v2)。highlight.jsは、200以上のプログラミング言語をサポートし、プログラムコードの言語を自動で検出してハイライトするといった機能があります。直近1年間のnpmダウンロード数を見ると、どちらも多くのサイトで利用されていることが伺えます。

Prism、highlight.js、Shikiの1年間のnpmダウンロード数を表したグラフ。
highlight.js vs prismjs vs shiki | npm trends

では、今回紹介するShikiはどのような特徴があるのでしょうか?

VSCodeと同じTextMate文法エンジン

ShikiはVSCodeのシンタックスハイライトと同じ、TextMateの文法とテーマをベースにしています。そのため、VSCodeの更新に伴ってShikiの文法とテーマも更新されます。執筆時点(2024/07/10)では、デフォルトで46のテーマと208の言語をサポートしており、カスタマイズしたテーマや言語を使用することもできます。

公式サイトでは、これらのテーマと言語をプレビューできるPlaygroundも用意されており、使用したいテーマを事前にプレビューすることができます。

あらゆるJavaScriptランタイム上で動作

Node.jsのAPIやファイルシステムに依存せず、ブラウザ、Node.js、Cloudflare Workersなど、最新のJavaScriptランタイムで動作します。例えば、Shikiのドキュメントサイトではコードブロックはビルド時にレンダリングし静的に配信され、前述のPlaygroundのみ、クライアントサイドでレンダリングされているようです。

高度なカスタマイズが可能

ハイライトするコードの指定範囲に、独自のクラスや属性を適用できる装飾用のAPIが提供されています。これにより、コードの特定箇所に枠をつけるなどの処理が簡単に行えるようになっています。

また、Shikiはhast(HTMLのASTフォーマット)を使用してHTMLを生成します。このhastを操作する処理(トランスフォーマー)を独自に書くことで、生成されるHTMLを柔軟にカスタマイズすることができます。

柔軟なバンドル

メインのShikiエントリーには、サポートされているすべてのテーマと言語がバンドルされています。文法が使用されるときにのみインポート・ダウンロードされるため、パフォーマンスに優れています。また、ブラウザのランタイムで使用する場合などに細かくコントロールしたい場合は、独自にバンドルを構成することも可能です。あらかじめ構成された以下の2つのバンドルも提供されています。

  • shiki/bundle/full【バンドルサイズ:6.4 MB(最小化)、1.2 MB(gzip)】

    メインのshikiエントリーと同じように、すべてのテーマと言語が含まれています。

  • shiki/bundle/web【バンドルサイズ:3.8 MB(最小化)、695 KB(gzip)】

    すべてのテーマと一般的なウェブ言語(HTML、CSS、JS、TS、JSON、Markdownなど)、いくつかのWebフレームワーク(Vue、JSX、Svelteなど)が含まれています。

基本の使い方

Shikiの特徴がなんとなく分かってきたと思いますので、ここから基本的な使用方法について紹介します。

導入

Shikiを利用するには、npm経由でインストールするか、CDNで利用することができます。今回はnpm経由での利用方法を紹介します。CDNでの利用については公式ドキュメントを参照してください。

npm install -D shiki

簡単な使用例

まずは、Shikiが提供するcodeToHtml関数を用いることで、簡単に使い始めることができます。言語とテーマを指定し、対象のコードをcodeToHtml関数に渡すと、ハイライトされたHTML文字列が返されます。

TypeScript
import { codeToHtml } from "shiki"

const code = "const a = 1" // 表示するコード
const html = await codeToHtml(code, {
  lang: "javascript", // 言語を指定
  theme: "github-light" // テーマを指定
})

HTML文字列には、各トークンのインラインスタイルが含まれているので、スタイルを設定するための追加のCSSは必要ありません。

戻り値のHTML文字列
<pre
  class="shiki github-light"
  style="background-color: #fff; color: #24292e"
  tabindex="0"
>
  <code>
    <span class="line">
      <span style="color:#D73A49">const</span>
      <span style="color:#005CC5"> a</span>
      <span style="color:#D73A49"> =</span>
      <span style="color:#005CC5"> 1</span>
    </span>
  </code>
</pre>

これを表示すると、以下のようになります。

装飾されたコードのキャプション:const a = 1

codeToHtml関数の他にも、codeToTokenscodeToHastを使用して中間データ構造を取得し利用することもできます。

参照:https://shiki.style/guide/install#usage

同期的にハイライトする

先ほど使用したcodeToHtmlは、テーマと言語をロードするために非同期に実行されます。コードを同期的にハイライトする場合は、getHighlighter関数を使用して事前にインスタンスを作成します。

TypeScript
import { getHighlighter } from "shiki"

const code = "const a = 1"

// 事前にインスタンスを作成
const highlighter = await getHighlighter({
  langs: ["javascript"],
  themes: ["github-light"],
})

// 作成したインスタンスを使用して同期的にハイライト
const html = highlighter.codeToHtml(code, {
  lang: "javascript",
  theme: "github-light"
})

また、ハイライト作成後にテーマや言語をロードしたい場合は、loadThemeloadLanguageメソッドを使うことができます。

TypeScript
// テーマを追加
await highlighter.loadTheme("github-dark")

// 言語を追加
await highlighter.loadLanguage("css")

すべてのテーマと言語を読み込みたい場合は、bundledLanguagesbundledThemesからすべてのキーを読み込むことができますが、こちらは推奨されていません。

参照:https://shiki.style/guide/install#highlighter-usage

発展的な機能

ライトモード・ダークモード(デュアルテーマ)

Shikiはライトモード・ダークモードのデュアルテーマ出力をサポートしています。

設定方法は、まずthemesオプションのlightdarkキーにそれぞれ利用したいテーマを指定します。

TypeScript
import { codeToHtml } from "shiki"

const code = "const a = 1"

const html = await codeToHtml(code, {
  lang: "javascript",
  themes: { 
    light: "github-light", // ライトモードのテーマを指定
    dark: "github-dark", // ダークモードのテーマを指定
  }
})

Shikiは各トークンの色を保存するためにCSS変数を使用しているため、以下のどちらかの方法でCSSスニペットを追加する必要があります。プレフィックスはオプションで変更することも可能です(デフォルトは--shiki-)。

CSSスニペット:クエリーベース
@media (prefers-color-scheme: dark) {
  .shiki,
  .shiki span {
    color: var(--shiki-dark) !important;
    background-color: var(--shiki-dark-bg) !important;
    /* フォントスタイルにも対応する場合は以下のオプションも追加 */
    font-style: var(--shiki-dark-font-style) !important;
    font-weight: var(--shiki-dark-font-weight) !important;
    text-decoration: var(--shiki-dark-text-decoration) !important;
  }
}
CSSスニペット:クラスベース
html.dark .shiki,
html.dark .shiki span {
  color: var(--shiki-dark) !important;
  background-color: var(--shiki-dark-bg) !important;
  /* フォントスタイルにも対応する場合は以下のオプションも追加 */
  font-style: var(--shiki-dark-font-style) !important;
  font-weight: var(--shiki-dark-font-weight) !important;
  text-decoration: var(--shiki-dark-text-decoration) !important;
}

これで、ライトモード・ダークモードによってテーマを変更することができるようになりました。

装飾されたコードのキャプション(ライトモード):const a = 1
装飾されたコードのキャプション(ダークモード):const a = 1

複数のテーマ

ライトモード・ダークモードのみではなく、2つ以上のテーマをサポートすることも可能です。themesオプションは、任意の数のテーマを指定することができます。defaultColorオプションでデフォルトのテーマを指定することもできます。

TypeScript
import { codeToHtml } from "shiki"

const code = "const a = 1"

const html = await codeToHtml(code, {
  lang: "javascript",
  themes: {
    light: "github-light",
    dark: "github-dark",
    dim: "github-dimmed",
    // ... 任意の数を追加可能
  },

  // オプションでデフォルトのテーマを指定
  defaultColor: "light",
})

コードの装飾

Shikiにはコードをハイライトするだけではなく、コードの特定箇所に独自のクラスを適用するなど、装飾を細かくコントロールするいくつかの機能があります。

Decorations

decorations APIを使用することで、独自のクラスや属性を指定の範囲に適用することができます。使用方法は、decorationsオプション内にstartendで適用範囲を指定し、propertiesに任意のプロパティを指定します。例として、highlighted-wordクラスを用意し、コード内の指定箇所に適用してみます。

TypeScript
import { codeToHtml } from "shiki"

const code = `
const greeting = "Hello World!"
console.log(greeting)
`.trim();

const html = await codeToHtml(code, {
  lang: "javascript",
  theme: "github-light",
  decorations: [
    {
      start: { line: 1, character: 12 }, // 開始位置を指定
      end: { line: 1, character: 20 }, // 終了位置を指定
      properties: { class: "highlighted-word" }, // 適用するプロパティを指定
    },
  ],
});
CSS
.highlighted-word {
  border: solid 1px #555555;
  padding: 0 2px;
}

表示結果は以下のようになります。2行目の13文字目から20文字目まで、「greeting」の箇所にhighlighted-wordのスタイルが適用されていることがわかります。

装飾されたコードのキャプチャ画像:const greeting = 'Hello World!' console.log(greeting)

Transformers

Shikiはhast(HTMLのASTフォーマット)を使用してHTMLを生成しています。Transformers機能を利用すると、このhastを操作する処理を独自に書くことができます。コードの特定の行にアンダーラインを引いたり、diffを表示するといったことが可能です。

使用方法は、transformersオプション内で変換用のHookを利用した処理を記述します。以下のようなHookが用意されており、任意のタイミングで処理を挟みむことができます。

出典:https://shiki.style/guide/transformers#transformer-hooks

例えば、特定の行に装飾を加えたい場合は、lineを使用します。

TypeScript
import { codeToHtml } from "shiki";

const code = `
const greeting = "Hello World!"
console.log(greeting)
`.trim();

const html = await codeToHtml(code, {
  lang: "javascript",
  theme: "github-light",
  transformers: [
    {
      // 2行目にアンダーラインを追加
      line(node, line) {
        if (line === 2) this.addClassToHast(node, "underline"); 
      },
    },
  ],
});

これで、以下のように2行目にアンダーラインを引くことができました。

装飾されたコードのキャプチャ画像:const greeting = 'Hello World!' console.log(greeting)

また、別パッケージとして提供される@shikijs/transformersには、いくつかの便利なTransformersが用意されています。こちらをインストールして利用することで、コードのdiff表示特定の行をハイライトするといったことが簡単に行えます。

npm i -D @shikijs/transformers

コードのdiff表示を追加したい場合は、transformerNotationDiffを使用します。transformersオプションにインポートしたtransformerNotationDiff()を追加し、コードの差分表示したい行の末尾に// [!code --]// [!code ++]をそれぞれ追加します。

TypeScript
import { codeToHtml } from "shiki";
import { transformerNotationDiff } from "@shikijs/transformers";

const code = `
const greeting = "Hello World!" // [!code --]
const greeting = "Hello Japan!" // [!code ++]
console.log(greeting)
`.trim();

const html = await codeToHtml(code, {
  lang: "javascript",
  theme: "github-light",
  transformers: [transformerNotationDiff()],
});

これで、// [!code ++]を加えた箇所は<span class="line diff add">// [!code --]を加えた箇所は<span class="line diff remove">とそれぞれclassが付与されたアウトプットが得られます。スタイル自体は生成されないため、好みのスタイルを追加します。

CSS
.line.diff.remove {
  background-color: #fef2f2;
}
.line.diff.remove::before {
  margin-right: 4px;
  content: "−";
  color: #dc2626;
}
.line.diff.add {
  background-color: #f0fdf4;
}
.line.diff.add::before {
  margin-right: 4px;
  content: "+";
  color: #16a34a;
}

表示結果は以下のようになりました。

装飾されたコードのキャプチャ画像:const greeting = 'Hello World!' // [!code --] const greeting = 'Hello Japan!' // [!code ++] console.log(greeting)

まだまだ進化するShiki

今回は、シンタックスハイライター「Shiki」の特徴と基本機能について簡単に紹介しました。

Shikiには今回紹介した機能以外にも、まだまだ便利なものが備わっています。いくつか用意されているインテグレーションを利用し、TowslashMonaco Editorなどを、機能統合して使用することもできます。また、最新のv1.10ではコード・スニペットの一部をハイライトしたい場合などに便利なGrammarStateの機能が追加されました。(参考:https://github.com/shikijs/shiki/pull/712

まだまだ進化するShikiを利用して、コードブロックに美しいハイライトを施してみてください!

参考資料

ウェブのお悩み、世路庵にご相談ください

ウェブ制作会社には、「言ったことしかやってくれない」「提案がない」といった不満を抱かれるケースがあります。目を引くようなビジュアルは作れるがビジネス理解が不足している、運用はしてもらえるがデザインやコーディングは外注に丸投げしている、といった体制では、しばしばプロジェクトが袋小路に迷い込んでしまいます。

世路庵は、ビジネスとクリエイティブを両立するウェブ制作会社です。ウェブサイトやウェブアプリケーションに課題を感じている方は、創業16年以上の経験と、業種・業態を選ばない800件以上の実績を持つ世路庵をぜひご検討ください。

合同会社世路庵

記事をシェアする

小山 樹人

フロントエンドエンジニア

静岡県出身。大学卒業後、桑沢デザイン研究所の夜間部でグラフィックデザインを学ぶ。専門誌や教育系の出版物を中心に、約10年間DTP制作の仕事に携わる。Webのフロントエンド開発の分野に興味を持ち、2023年に世路庵に入社。 デザインと開発の両面からものづくりに携わっていきたい。