🚥

シンタックスハイライター`Shiki`の紹介

2024/03/27に公開

shiki とは何か

shiki は、VS Code のシンタックスハイライトと同じエンジンである TextMate の文法とテーマをベースにした、高度なカスタマイズが可能なシンタックスハイライターです。

Astro でも内部で使われていたり Node.js のWebsiteでも使用されていたりします。
https://twitter.com/antfu7/status/1770383637579517989

この記事では、shiki の使い方や特徴について紹介していきますが、とりあえず試したい方は公式ドキュメントにPlaygroundが用意されているので使ってみるのもいいかもしれません。

前提:
この記事の情報はv.1.2.0の公式ドキュメントを参考にしています。

shiki の名前の由来は?

日本語の「式(Style)」から来ているそうです。確かにこのライブラリのロゴも「式」に見えますね。

shikiji とは何が違うの?

shiki は元々antfuさんのレポジトリに存在するshikijiが元になっています。(shikiji自体は現在アーカイブになっています。)

特徴と使用例

最小で試す

pnpm add -D shiki

shiki を使い始める最も手っ取り早い方法である、提供されているcodeToHtmlを使います。

import { codeToHtml } from "shiki";
import { CODE } from "@/constants";

export default async function DemoPage() {
  const html = await codeToHtml(CODE, {
    lang: "tsx",
    theme: "github-dark-dimmed",
  });

  return <div dangerouslySetInnerHTML={{ __html: html }}></div>;
}

結果はこうなりました。いけてますね。

ちなみにcodeToHtmlの返り値をみてみましょう。

<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"> greeting</span>
      <span style="color:#D73A49"> =</span>
      <span style="color:#032F62"> "Hello, World!"</span>
      <span style="color:#24292E">;</span>
    </span>
    <span class="line"></span>
    <span class="line">
      <span style="color:#24292E">console.</span>
      <span style="color:#6F42C1">log</span>
      <span style="color:#24292E">(greeting); </span>
      <span style="color:#6A737D">// Hello, World!</span>
    </span>
  </code>
</pre>

こんな感じで生成された HTML にはインラインでスタイルが適用されます。別で CSS を持つ必要がないのでとても楽です。(後述のダークモードの対応には CSS のスニペットが必要になってきます)

Highlighter を使用して同期的にハイライトする

上述のcodeToHtmlは内部でテーマと言語をオンデマンドでロードするために非同期で実行されます。しかし、getHighlighterを使用することで、同期的にハイライトすることができます。

import { getHighlighter } from "shiki";
import { CODE } from "@/constants";

export default async function HighlighterPage() {
  const highlighter = await getHighlighter({
    themes: ["github-light"],
    langs: ["tsx"],
  });

  const html = highlighter.codeToHtml(CODE, {
    lang: "tsx",
    theme: "github-light",
  });

  return <div dangerouslySetInnerHTML={{ __html: html }}></div>;
}

同様の結果が得られました。

Light / Dark テーマサポート

shiki は、Light / Dark テーマをサポートしています。themesプロパティに複数のテーマを指定することで、テーマを切り替えることができます。

import { codeToHtml } from "shiki";
import { CODE } from "@/constants";

export default async function MyThemePage() {
  const html = await codeToHtml(CODE, {
    lang: "tsx",
    themes: {
      // lightとdarkのテーマを指定
      light: "github-light",
      dark: "github-dark-dimmed",
    },
  });

  return <div dangerouslySetInnerHTML={{ __html: html }}></div>;
}

注意点として、ダークモードを利用するには以下のような CSS スニペットを追加する必要があります。

html.dark .shiki,
html.dark .shiki span {
  color: var(--shiki-dark) !important;
  background-color: var(--shiki-dark-bg) !important;
}

こんな感じになりました。


Light モード

Dark モード

shiki は Light / Dark モード だけでなく、より多くのテーマを設定することもできます。詳しくはこちらを参照してください。

デコレーション

カスタムクラスやカスタム属性を付与することができます。

import { codeToHtml } from "shiki";
import { CODE } from "@/constants";

export default async function DecorationsPage() {
  const html = await codeToHtml(CODE, {
    lang: "tsx",
    theme: "github-dark-dimmed",
    decorations: [
      // "const"の背景色を赤、文字色を白にする
      {
        start: 0,
        end: 5,
        properties: {
          class: "bg-red-500 !text-white",
        },
      },
      // 変数名"greeting"に緑のborderを付与する
      {
        start: 6,
        end: 14,
        properties: {
          class: "border-2 border-green-500",
        },
      },
    ],
  });

  return <div dangerouslySetInnerHTML={{ __html: html }}></div>;
}

以下のようにクラスを当てたい箇所にstartendを指定することで、その範囲にクラスを適用することができました。

Transformers

shiki は hast(HTML を AST(抽象構文木)として表現するための仕様)をサポートしており、独自のトランスフォーマーを書くことで、生成される HTML をカスタマイズすることができます。

import { codeToHtml } from "shiki";
import { CODE, DEFAULT_LANG_AND_THEMES } from "@/constants";

export default async function TransformersPage() {
  const html = await codeToHtml(CODE, {
    ...DEFAULT_LANG_AND_THEMES,
    transformers: [
      {
        line(node, line) {
          // 奇数行に下線を追加
          if (line % 2 === 1) this.addClassToHast(node, "underline");
        },
      },
    ],
  });

  return <div dangerouslySetInnerHTML={{ __html: html }}></div>;
}

結果はこんな感じになりました。

奇数行に下線を追加

line以外にも様々な Hooks が提供されているので詳しくみたい方はこちらを参照してください。

また、@shikijs/transformers というパッケージに含まれている transformers を使用することも可能で、コードの diff を表示するためのtransformerNotationDiff()やコードにフォーカスのスタイルを当てることができるようにするtransformerNotationFocus()など便利な Transformers が用意されています。

pnpm add -D @shikijs/transformers
import { codeToHtml } from "shiki";
import { transformerNotationDiff } from "@shikijs/transformers";

export default async function TransformersPage() {
  /**
   * [!code --] が付与されたlineは削除を表す
   * [!code ++] が付与されたlineは追加を表す
   */
  const code = `
  console.log('hewwo') // [!code --]
  console.log('hello') // [!code ++]`;
  const html = await codeToHtml(code, {
    lang: "tsx",
    theme: "github-light",
    transformers: [transformerNotationDiff()],
  });

  return <div dangerouslySetInnerHTML={{ __html: html }}></div>;
}


transformerNotationDiff()の使用例

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

export default async function TransformersPage() {
  /**
   * [!code focus] が付与されたlineはフォーカスされた状態を表す
   */
  const code = `
  console.log('Not focused');
  console.log('Focused') // [!code focus]
  console.log('Not focused');`;
  const html = await codeToHtml(code, {
    lang: "tsx",
    theme: "github-light",
    transformers: [transformerNotationFocus()],
  });

  return <div dangerouslySetInnerHTML={{ __html: html }}></div>;
}


transformerNotationFocus()の使用例

注意点として、Transformers は、クラスを適用するだけでスタイルは付属していないので、自分でスタイルを適用する必要があります。
以下の例は、transformerNotationDiff()を使用した場合のスタイルを適用する例です。生成された HTML にdiff removediff addの class が付与されて返却されるのでそこにスタイルが当たるようにしています。

<pre
  class="shiki github-dark-dimmed has-diff"
  style="background-color:#22272e;color:#adbac7"
  tabindex="0"
>
    <code>
        <!-- 赤背景にする -->
        <span class="line diff remove">
            <span style="color:#ADBAC7">console.</span>
            <span style="color:#DCBDFB">log</span>
            <span style="color:#ADBAC7">(</span>
            <span style="color:#96D0FF">'hewwo'</span>
            <span style="color:#ADBAC7">) </span>
        </span>
        <!-- 緑背景にする -->
        <span class="line diff add">
            <span style="color:#ADBAC7">console.</span>
            <span style="color:#DCBDFB">log</span>
            <span style="color:#ADBAC7">(</span>
            <span style="color:#96D0FF">'hello'</span>
            <span style="color:#ADBAC7">)</span>
        </span>
    </code>
</pre>
.diff.remove {
  background-color: red;
}
.diff.add {
  background-color: green;
}

Theme Colors Manipulation

テーマを作成して使用したり、既存のテーマの一部の色を変更変更したりすることができる機能です。
自分の好みやプロジェクトのテーマに合わせて細かい調整ができそうです。詳しくはこちらを参照してください。

統合

いくつか用意されていますが個人的に面白いなと思ったのは TypeScript Twoslashです

TypeScript Twoslashはコードにカーソルを置くと型を表示できる機能です。
vite の公式ドキュメントでは v5.2 のアプデートから@shikijs/vitepress-twoslashを使用することでこの機能を導入しています。

https://twitter.com/sapphi_red/status/1770681546212790308

終わりに

以上、個人的に今来ていると感じるシンタックスハイライター shiki の紹介でした。
間違い等あればコメントにてお願いします。

参考

https://shiki.style/
https://github.com/syntax-tree/hast
https://github.com/shikijs/twoslash

ファンタラクティブテックブログ

Discussion