【React + Vite】Prism.jsでコードハイライトを実装する
はじめに
この記事では、Viteで動いているReactプロジェクトに、Prism.jsというライブラリを使ってコードハイライトを実装します。
この際、以下の方針で進めます。
- ハイライトする言語は動的に決める
- 既存のCDNは使わない
- Prism.js以外のライブラリは導入しない
Prism.jsの使い方
Prism.jsでコードハイライトをするには、以下が必要です。
- Prism.js本体: 言語ファイルを元にコードを解析し、
span
タグを追加する - 言語ファイル: それぞれの言語の解析方法が書かれたファイル
- カラーテーマ: 解析された
span
タグを元にスタイリングする
これらはPrism.js内に含まれているので、自分で用意する必要はありません。
詳細は過去に記事にしているので、こちらをご覧ください。
Prism.jsを使ってみる
まずはPrism.jsを動かし、最低限のハイライトができるようにします。
インストール
Prism.jsを使うには2つの方法があります。
-
prismjs
パッケージを使う - CDNを使う
今回はCDNを使わない方針のため、prismjs
パッケージをインストールして使います。
bun add prismjs @types/prismjs
Highlight
コンポーネントを作る
続いて、渡されたコードをハイライトするHighlight
コンポーネントを作ります。
このコンポーネントは以下のpropsを受け取ります。
-
children
: ハイライトしたいコードの文字列 -
lang
: コードの言語、必須
また、以下の処理を行います。
-
useRef
を使ってcode
要素のDOMオブジェクトを取得する -
useLayoutEffect
で↓の処理を副作用として実行する - Prism.jsの
highlightElement
関数を、1を使って呼び出す
function Highlight() {
// code要素が入るref
const ref = useRef<HTMLElement>(null)
useLayoutEffect(() => {
// 要素をハイライトする
if (ref.current) highlightElement(ref.current)
}, [children, lang])
return (/* 略 */)
}
useLayoutEffect
useLayoutEffect
はuseEffect
と大体同じですが、副作用の実行タイミングが少し違います。
-
useEffect
: ブラウザが画面を再描画した後に実行される -
useLayoutEffect
: ブラウザが画面を再描画する前に実行される
今回の場合、highlightElement
の呼び出しにはDOMオブジェクトが必要です。
ですが、ハイライト前の要素をブラウザに描画してほしくはありません。
そのため、その中間で実行されるuseLayoutEffect
を使っています。
実装
これを実装したものがこちらです。
import { highlightElement } from "prismjs";
import { useLayoutEffect, useRef } from "react";
export default function Highlight({ children, lang }: {
children: string
lang: string
}) {
const ref = useRef<HTMLElement>(null)
useLayoutEffect(() => {
if (ref.current) highlightElement(ref.current)
}, [children, lang])
return (
<pre>
<code className={`lang-${lang}`} ref={ref}>
{children}
</code>
</pre>
)
}
カラーテーマを選ぶ
Prism.jsを使うには、カラーテーマ(各トークンの色を指定するCSSファイル)が必要です。
これは自作することもできますが、今回は配布されているものを使います。
カラーテーマのCSSファイルはprismjs
パッケージ内に含まれているので、import
文を追加するだけで読み込むことができます。
// カラーテーマを読み込む
import "prismjs/themes/prism.min.css";
カラーテーマ一覧は公式サイトの右ボタンから見ることができます。
準備ができたので、Highlight
コンポーネントを使ってみます。
呼び出し例はこちらです。
export default function App() {
return (
<main>
<p>普通の文章</p>
<Highlight lang="js">
console.log('Hello World!')
</Highlight>
</main>
)
すると、ブラウザに以下が表示されると思います。
プラグインを追加する
Prism.jsのプラグインにはいろいろなものがありますが、ここでは下記を導入します。
-
autoloader
: 必要な言語ファイルを自動で読み込む -
normalize-whitespace
: 余分な空白を削除する
一覧はこちら。
やり方
これらのプラグインはPrism.jsのnpmパッケージに含まれています。
そのため、普通にimport
するのが一番簡単です。
// カラーテーマ
import "prismjs/themes/prism.min.css";
// プラグイン
import "prismjs/plugins/autoloader/prism-autoloader.js";
import "prismjs/plugins/normalize-whitespace/prism-normalize-whitespace.min.js";
外部ライブラリを活用する
プラグインやカラーテーマの設定を簡単に行う方法として、vite-plugin-prismjs
があります。
これはbabel-plugin-prismjs
をVite向けにラップしたもので、設定をvite.config.ts
から行えます。
設定例:
// ...
import prism from 'vite-plugin-prismjs'
export default defineConfig({
plugins: [
react(),
prism({
plugins: ['autoloader', 'normalize-whitespace'],
theme: 'tomorrow',
css: true,
})
],
})
normalize-whitespace
の効果
このプラグインは、コードに余分な余白が含まれていた場合削除してくれます。
例えば、以下のようにコードを2行に増やしてみます。
Reactの仕様?によりデフォルトだと改行が反映されないため、文字列になるよう`
で囲っています。
<Highlight lang="js">{`
const message = 'Hello World!'
console.log(message);
`}</Highlight>
normalize-whitespace
を導入していない場合、余分な空白が上下左右に入ってしまいます。
しかし導入している場合、以下のように余分な空白がない状態で表示されます。
autoloader
が動くように設定する
先ほど導入したautoloader
ですが、実は今のままだと動いていません。
これは言語ファイルへのパスが間違っているためです。
このままだとTypeScriptやTSXなどがハイライトできないので、修正する必要があります。
なおautoloader
は、動的に言語ファイルを読み込むためのプラグインです。
言語ファイルは各言語のハイライト方法が書かれたファイルで、ハイライトにはそのコードの言語に対応した言語ファイルが必要になります。
通常、autoloader
は/components
というURLから言語ファイルを探します。
しかし、今はVite上で動かしているため、そもそも言語ファイルは公開されていません。
対処方法
この問題に対処する方法は2つあります。
-
localhost
に言語ファイルを公開する - 既存のCDNを活用する
今回は既存のCDNは使わない方針のため、前者を使います。
具体的には以下のようにします。
- Viteの静的アセットとして
node_modules
にある言語ファイルを配信する -
autoloader
の言語ファイルのパスを設定する - TypeScriptなどデフォルトだとない言語をハイライトしてみる
言語ファイルを配信する
これは意外と簡単で、public
ディレクトリ内の好きなパスにファイルをコピーするだけです。
ここではpostinstall
を使い、bun install
(npm install
)が行われたときにコピーされるようにしています。
"scripts": {
// ...
"postinstall": "bun run add-prism-languages",
"add-prism-languages": "mkdir -p public/prismjs/components/ && cp -r node_modules/prismjs/components/ public/prismjs/components/"
}
設定が終わったら、一度bun add-prism-languages
かbun install
してコピーしてください。
パスを設定する
次に、言語ファイルまでのパスを設定します。
設定はPrism
オブジェクトのplugin.autoloader?.languages_path
プロパティから変更できます。
import { highlightElement, plugins } from "prismjs";
// autoloaderの言語ファイルのパスを設定
plugins.autoloader?.languages_path = "/prismjs/components/"
なお、この設定は↑で配信した言語ファイルのパスと揃えてください。
試しにカウンターのコードをハイライトしてみると、このようになると思います。
export default function App() {
return (
<main>
<p>普通の文章</p>
<Highlight lang="jsx">{`
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<h3>{count}</h3>
<button onClick={() => setCount(count + 1)}>add</button>
</div>
)
}
`}</Highlight>
</main>
)
}
参考
Discussion