🐷

Reactアプリ上でソースコードを色つき表示するならOSSライブラリ? Prism.js? Highlight.js? それとも?

2024/10/01に公開

導入

ソースコードを色つき表示することは、視覚的な美しさのみならず、ユーザーにとっての読みやすさのためにとても重要です。

本記事では React アプリケーション上でソースコードを色つき表示するために使えるライブラリ、ツールなど多数の選択肢からひとつを選ぶため

  • 選択肢を絞り込む判断基準
  • 色つき表示を行う、主なライブラリやツール

を紹介します。

ソースコードを色つき表示する仕組み

React アプリケーション上でソースコードを表示する場合、多くの開発者が一番最初に<pre><code>タグを思い浮かべると思います。しかし、<pre><code>タグはレイアウトはしてくれるものの、色はつけてくれないので、そのままでは読みづらい表示になってしまいます。

色なし、<pre><code>だけで表示した「Go言語のhello world」
package main

import "fmt"

func main() {
    fmt.Println("hello world")
}
色つきとの比較
package main

import "fmt"

func main() {
    fmt.Println("hello world")
}

React に限りませんが、HTML と CSS を使ってソースコードを色つき表示するには、ソースコード内の単語一つ一つを<span>で囲んで、classなど (React の JSX ではclassName) によって CSS を適用します。

HTMLとCSSで色つき表示するためには<span>で囲む
<pre>
  <code className="language-go code-line" data-line="26">
    <span className="token keyword">package</span> main
    <span className="token keyword">import</span> <span className="token string">"fmt"</span>
    <span className="token keyword">func</span> <span className="token function">main</span><span className="token punctuation">(</span><span className="token punctuation">)</span> <span className="token punctuation">{</span>
        fmt<span className="token punctuation">.</span><span className="token function">Println</span><span className="token punctuation">(</span><span className="token string">"hello world"</span><span className="token punctuation">)</span>
    <span className="token punctuation">}</span>
  </code>
</pre>

当然ながら一つ一つの<span>を手作業で書くわけにはいかないので、何らかのライブラリやツールに頼ることになります。

ライブラリやツールが多く迷ってしまうので、判断基準が必要

しかし、何らかのライブラリやツールに頼る際に選択肢が多すぎることが React 開発者にとっての悩みになってしまいます。

そこで本記事では、React 開発者の悩みを減らせるよう、これらの選択肢から一つを選び出すための判断基準を紹介します。

まず重要な判断基準が、「表示するソースコードは Editable か?Read-Only か?」 です。

Read-Only で十分であれば、React Syntax Highlighter のようなシンプルかつ効果的な選択肢が利用可能です。しかし、Read-Only ではなく Editable なソースコードを実現するツール群は、一般に使い方が複雑であったり、JavaScript のファイルサイズが大きくなりがちです。このため、自身が開発する React アプリケーションにおいて「本当に Editable である必要があるのか?」を慎重に検討することが求められます。実際、ユーザーが手動でソースコードを記述する必要性は、開発者が想定するほど多くはなく、ユーザーはブラウザ上でのソースコード記述を望まないものです。

次に考慮すべき重要な判断基準は、「現代の React に対応しやすいか?」 という点です。

現在の React アプリケーションでは、Server Side Rendering や将来的に普及が期待される Server Component など、サーバー側での処理が一般的です。そのため、Browser API に依存してクライアントサイドでのみレンダリングを行う選択肢は、開発者にとって扱いづらいと感じられることがあります。特に現代の React に対応しづらい選択肢は、慎重に注意点を考慮した上で利用判断を行うことが求められます。

本記事では Read-Only 系の選択肢のみ紹介し、Editable 系の選択肢は後日別記事にて紹介する予定です。

Read-Only 系の選択肢

React アプリ上でソースコードを色つき表示する必要があっても、Read-Only ならば Editable に比べて相当に複雑さが軽減されます。一般にブラウザ上でテキストエディタを実現するのは非常に難度が高いと考えられており、たとえ既存のブラウザ内エディタツールを利用するにしても、エディタツールと React アプリを連携する際に事前の想定以上に苦労する可能性があります。

本記事では Read-Only で十分な想定で選択肢を紹介し、そのうえで React Syntax Highlighter で対応可能な範囲であるなら React Syntax Highlighter をおすすめしています。

React Syntax Highlighter

  • Read-Only な選択肢には ◎
  • 一瞬で導入できる
  • 現代的な React にも対応 ◎
  • 色つき表示「以上」の機能を求めると足りないことも

React でソースコードを色つき表示 (Syntax Highlight) する OSS ライブラリとしては最も有名で、かつ本記事で紹介する選択肢の中でも最も導入が簡単なもののひとつです。本記事でも一番のおすすめとして紹介します。

公式の GitHub レポジトリにもあるように以下の npm コマンドを実行して(本記事では TypeScript 用に2行目にある@types のインストールも行っています。)

npm install react-syntax-highlighter --save
npm install @types/react-syntax-highlighter --save-dev

このようなコンポーネントを書けば、すぐにソースコードを色つき表示できてしまいます。

import SyntaxHighlighter from "react-syntax-highlighter";
import { docco } from "react-syntax-highlighter/dist/esm/styles/hljs";

const Component = () => {
  const codeString = "(num) => num + 1";
  return (
    <SyntaxHighlighter language="javascript" style={docco}>
      {codeString}
    </SyntaxHighlighter>
  );
};

Browser API に依存していないので現代的な React にも対応ずみです。Server Side Rendering でも問題なく動作しますし、Next.js 14 以降では普通のコンポーネントを書くだけで自動的に Server Component として使うことができます。サーバー側での処理のみで完結してしまうので、hooks や状態管理など難しいことを考える必要もありません。Server Component によって React Syntax Highlighter の JavaScript bundle サイズを気にする必要もなくなります。

bundle サイズ を気にして対象言語を絞り込む必要がなくなります

つまり、Server Component であれば、こちらの「Light Build」に記載のような対策が必要なくなります。

https://github.com/react-syntax-highlighter/react-syntax-highlighter?tab=readme-ov-file#light-build

しかし、React Syntax Highlighter に備わっている「以上」の機能を求めると苦労する場合があります。例えば「指定した行をハイライトしたい」というのはよくある要求ですが、React Syntax Highlighter は対応していません。自分で JavaScript を書いて機能を実現する必要があり、↓ こちらの記事にあるような工夫が求められます。

https://zenn.dev/richardimaoka/articles/72093eac05b6f5

また React Syntax Highlighter はソースコード表示部分に重ねるようにボタンやツールチップを配置するのが難しいため、こういった「自由自在に追加コンポーネントを配置する」場合には代替の選択肢が必要になってきます。

Prism.js と Highlight.js

  • Read-Only な選択肢には ◎
  • 現代的な React 対応 △
  • 結局 React Syntax Highlighter と同じものを再発明することになりがち

Prism.js と Highlight.js はソースコードの色つき表示のためのツールとしては最も有名な2つです。どちらも React 登場以前から存在し、安定した仕様と実装に加え、非常に幅広いアプリケーションでの利用実績があります。

実は React Syntax Highlighter も内部では Prism.js と Highlight.js を使っています。Prism.js か Highlight.js を直接使うよりは React Syntax Highlighter を経由して使うほうが、現代的な React への対応もでき、React アプリへの導入も簡単になります。そのため本記事では React Syntax Highlighter を使うことをおすすめしています。「それでも直接 Prism.js か Highlight.js を使うほうがシンプルで良いのでは?」 と考える人のために、ここからはその際に考慮すべき課題を紹介します。

冒頭に記載の通り、ソースコードの色つき表示は<pre><code>タグで囲んだ内部に<span>タグを大量に追加する必要があるのですが、<span>タグの追加を全部自力で実装するのは大変です。Prism.js と Highlight.js はまさにその機能を提供してくれるツールです。

最も簡単な導入方法は HTML 内で<link>タグによる CSS 読み込み、<script>タグによる JavaScript の読み込みを行うだけです。以下は導入方法を省略した形ですが、完全なものでも数行で済んでしまいます。

<link href="themes/prism.css" rel="stylesheet" />
...
<!-- HTML内のすべての<pre><code>を色つき表示 -->
<script src="prism.js"></script>
<!-- Highlight.jsの場合は <script>hljs.highlightAll();</script> -->

ただし、上記の方法はそのまま React で使うことは少なく、以下のようにuseEffect内で、かつ色つき表示する<code>要素を指定したうえで使うことが多いでしょう。

useEffect(() => {
  Prism.highlightElement(elem); //elemは<code>要素
  //Highlight.jsの場合 hljs.highlightElement(elem)
},

この方法にもまだ課題はあり、useEffect()は初回レンダリング後に呼ばれるため、初回レンダリングの色なし表示とuseEffect()後の色つき表示の間で画面のチラつきが発生する可能性があります。また、React の仮想 DOM 管理の外の世界で DOM ツリーを書き換えてしまうため、それが気になってしまう開発者もいるでしょう。

useEffectを避け、さらに現代の React らしく Server Side Rendering や Server Component といったサーバー側処理を活かそうとすると、Prism.js や Highlight.js の Node API を使うことになります。使い方は以下のとおりですが、問題はどちらも出力が HTML 「文字列」になってしまうことです。

//Prism.js, htmlはHTML文字列
const html = Prism.highlight(code, Prism.languages.javascript, "javascript");

//Highlight.js, htmlはHTML文字列
const html = hljs.highlightAuto(code).value;

HTML 文字列はそのままでは React コンポーネントにはならないため、React のdangerouslySetInnerHTMLを使うことになります。dangerouslyという名前から想起される通り、これを積極的に使いたがる開発者は少ないでしょう。

まとめると Prism.js や Highlight.js を React アプリで直接使う場合以下のどちらかを選択することになり、どちらにも課題が残ってしまいます。

  • サーバー側でdangerouslySetInnerHTMLを呼び出す
  • クライアント側でuseEffect()の内部から Prism.highlightElement()hljs.highlightElement()を呼び出す

これらの課題を解決しようと作り込みをしていると、結局は React Syntax Highlighter が内部で行っていることと同じものを再発明することになりがちです。

一方で課題の解決は目指さず、多少の問題に目をつむるなら Prism.js や Highlight.js を React と組み合わせて使えるかもしれませんが、それなら React Syntax Highlighter を使えばもっと簡単なうえ、上記の問題も避けられます。これが先ほど「Prism.js か Highlight.js を直接使うのをすすめない」と言った理由になります。

自力で<span>追加

  • Read-Only な選択肢には ◎
  • 現代的な React 対応 ◎
  • 上級者向けの選択肢で、高度な実装能力が要求される

これは 上級者向けの選択肢 であり、かつ高度な実装能力と多くの開発時間を要します。そこまでの自信と時間的余裕がない場合は、React Syntax Highlighter の提供する機能のみで妥協するのが、本記事としてはおすすめです。それでも自力実装したいというチャレンジ精神あふれる開発者は、ぜひ以下の文章におつき合いいただければと思います。AST のような概念に触れながら開発できる楽しい機会になると思います。

この選択肢を選ぶということは、React Syntax Highlighter では実現しづらい機能が必要 なのだと思います。ではなぜ React Syntax Higlighter では実現しづらい機能があるのか?というと「<pre><code>タグの内部に自由にアクセスできない」ことが大きな理由だと考えられます。内部に自由にアクセスできないので「ソースコードの特定行に重なるように要素を配置する」といったことが難しくなります。(以下の画像は再掲)

自力でソースコードの色つき表示を実装する場合<pre><code>タグで囲んだ内部に<span>タグを追加するために、大まかには以下のような JSX を記述することになります。

return (
  <pre className="...">
    <code>
      {srcCodeTokens.map(token => <span key={...} className={...}>{...}</span>)}
    </code>
  </pre>

ソースコードをトークンに分割し、トークンごとに<span>で囲んで色付き表示します。Prism.js の tokenize 関数の説明に「This is the heart of Prism」とあるように、ソースコードの構文解析とトークンの生成こそがソースコードを色付き表示する際の最重要ロジックです。

自力で実装する場合も、React Syntax Highlighter が内部で行っていることが優れた参考例になります。そこで、React Syntax Highlighter がどのように Prism.js と Highlight.js を利用しているか簡単に紹介します。

React Syntax Highlighter では HTML 文字列ではなく JavaScript object で表現された AST (Abstract Syntax Tree, 抽象構文木) を扱うために、refractor (for Prism.js) と lowlight (for Higlight.js)を使っています。

Syntax highlighting component for React using the seriously super amazing lowlight and refractor by wooorm - React Syntax Highlighter (GitHub)

自力実装でも、トークン生成から AST 生成まではツールに頼ったほうが良いので、refractor(for Prism.js) か lowlight(for Higlight.js)を使いましょう。あとはReact Syntax Highlighter のソースコードを参考にして開発を進められます。もし Next.js を使っているならデバッガーの設定を行えば、React Syntax Highlighter のソースコードの動作を調べやすくなります。

Gist と GitHub embed (埋め込みサービス)

  • Read-Only な選択肢には ◎
  • 現代的な React に対応は ✕
  • GitHub 公式の埋め込みサービスは Gist のみ、そして自由度が低い
  • React コンポーネントの OSS を探すか、自分で実装することになる可能性が高い

ここで言う「埋め込みサービス」とは、外部サービスに投稿したソースコードを自分の React アプリに表示できるものを指します。

上記のように埋め込みサービスはたくさんあるのですが、Read-Only な選択肢としては、GitHub のみを紹介します。埋め込み系サービスとしては CodePen や CodeSandbox などのほうが使い勝手はいいのですが、それらは後日 Editable な選択肢を扱う別記事の方で紹介する予定です。

実は、GitHub の本サイト(https://github.com) には埋め込み機能がないため、公式の機能だけで埋め込みを行うには Gist (https://gist.github.com/) を使用する必要があります。Gist への投稿自体は数秒で済むので大きな負担にはなりませんが、どちらかというとその柔軟性の低さが問題です。

Gist 公式の埋め込みコードは以下のようにシンプルな<script>タグ 1 つのみです。

<script
  src="https://gist.github.com/richardimaoka/5069d6448bb6245b579a661afdffcd47.js">
</script>

導入が手軽なのはいいのですが、問題なのはこのスクリプトが内部で document.write (MDN) を利用して DOM ツリーの最後尾に埋め込み部分を追加することです。つまり、埋め込み位置をどこにするか指定できないので、事実上「利用できない」という判断をすることの方が多いでしょう。(Browser API であるdocument.writeに依存しているので、Server Side Rendering や Server Component との相性も悪いです。しかし埋め込み位置を指定できない方がよほど大きな問題です。)

GitHub/Gist の公式ツール埋め込みが使いづらいため、何らかの React コンポーネント・ライブラリ (OSS) が使えるか?と探してみたのですが、React Syntax Highlighter と比べると、十分に有名でよく保守されているものは見つけられませんでした。既存ツールを使わずに自分で実装するとしたら、わざわざ Gist や GitHub と連携する意味合いは薄れてくるでしょう。

まとめ

以上より、本記事としては React 上のソースコード表示について「できるだけ React Syntax Highlighter の機能の範囲で満足するように妥協する」、それがどうしても難しければ「自作しつつ refractor (for Prism.js) と lowlight (for Higlight.js)を活用する」というおすすめを紹介しました。

GitHubで編集を提案

Discussion