Next.jsのMDXでシンタックスハイライト付き差分表示を実装する(@shikijs/transformers)

背景:前回の記事で予告していた差分表示を実装

以前の記事「Shiki と rehype-pretty-code でMDXのシンタックスハイライトを移行した記録」で、差分表示については以下のように触れていた。

注意: diff-tsx のような複合指定は Shiki では非対応。差分を表現したい場合は、@shikijs/transformerstransformerNotationDiff を使用するとのこと。この辺はまた別記事としてまとめたい。

今回、実際に @shikijs/transformerstransformerNotationDiff を導入し、MDXでシンタックスハイライト付きの差分表示を実現した。

この記事では、その実装手順を記録する。

環境

実装前の状態

このプロジェクトでは既に rehype-pretty-codeshiki を使用してシンタックスハイライトを実装済みだった(前回の記事で導入)。

しかし、差分表示のための transformerNotationDiff はまだ設定していなかったため、コードの変更箇所を視覚的に表現できない状態だった。

実装手順

1. @shikijs/transformersパッケージをインストール

まず、@shikijs/transformersパッケージをインストールした。

yarn add @shikijs/transformers

2. next.config.mjsに transformers オプションを追加

next.config.mjsを更新し、rehype-pretty-codeの設定にtransformerNotationDiffを追加した。

import createMDX from '@next/mdx'
import remarkGfm from 'remark-gfm'
import rehypePrettyCode from 'rehype-pretty-code'
import { transformerNotationDiff } from '@shikijs/transformers'
 
/** @type {import('next').NextConfig} */
const nextConfig = {
  trailingSlash: true,
  turbopack: {
    root: process.cwd(),
  },
  experimental: {
    ppr: false,
  },
  pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
}
 
const withMDX = createMDX({
  remarkPlugins: [remarkGfm],
  rehypePlugins: [],
  rehypePlugins: [
    [
      rehypePrettyCode,
      {
        theme: 'github-dark',
        transformers: [transformerNotationDiff()],
      },
    ],
  ],
})
 
export default withMDX(nextConfig)

今回のプロジェクトでは@next/mdxを使用していたので、next.config.mjsrehypePluginsに設定を追加した。

既にremark-gfmを使用していたため、remarkPluginsはそのままで、rehypePluginsrehype-pretty-codetransformerNotationDiffを設定した。

3. 差分表示のスタイリング

次に、app/globals.cssに差分表示用のスタイルを追加した。

/* 差分表示用のスタイル */
[data-line].diff {
  --diff-bg-color: oklch(0.75 0.11 var(--diff-hue) / 0.15);
 
  position: relative;
  padding-left: 1rem;
  border-image: linear-gradient(var(--diff-bg-color), var(--diff-bg-color)) fill 0 / 0 / 0 50px;
 
  &::before {
    content: var(--diff-content);
    position: absolute;
    left: 0;
    color: oklch(0.58 0.09 var(--diff-hue));
  }
}
 
/* 追加行 */
[data-line].diff.add {
  --diff-hue: 157.92;
  --diff-content: "+";
}
 
/* 削除行 */
[data-line].diff.remove {
  --diff-hue: 18.82;
  --diff-content: "-";
}

使用方法

MDXファイル内で、コードブロックに// [!code ++]// [!code --]を記述することで差分を表現できる。

基本的な使い方

```js
function example() {
  console.log("古いコード"); // [\!code --]
  console.log("新しいコード"); // [\!code ++]
}
```

実際の表示例

function example() {
  console.log("古いコード");
  console.log("新しいコード");
 
  const oldValue = 42;
  const newValue = 100;
}

活用例

TypeScriptの型定義の変更

interface User {
  id: number;
  name: string;
  firstName: string;
  lastName: string;
  email: string;
  age: number;
  birthDate: Date;
}

SQLクエリの修正

-- 旧クエリ
select id, name from users; 
 
-- 新クエリ
select id, first_name, last_name from users; 

Reactコンポーネントのリファクタリング

export function Button({ label }: { label: string }) {
  return <button>{label}</button>;
  return (
    <button className="btn-primary">
      {label}
    </button>
  );
}

transformerNotationDiffの仕組み

transformerNotationDiff()は、コード内の[!code ++][!code --]というマーカーを検出し、以下のようにHTMLを変換する。

これにより、CSSセレクタで簡単にスタイリングできる。

この実装のメリット

可読性の向上

プレーンテキストの差分と異なり、シンタックスハイライトが維持されるため、コードの意味を理解しやすい。

技術記事に最適

リファクタリングやバグ修正の説明が視覚的に分かりやすくなり、読者が変更箇所を一目で把握できる。

簡単な記法

コメントとして記述するため、コピー&ペーストしても元のコードとして有効なまま使える。

複数言語対応

JavaScript、TypeScript、SQL、CSS、Pythonなど、shikiが対応しているあらゆる言語で使用可能。

まとめ

参考リンク