Prism.jsは、プログラムコードの構文を解析して、その言語ごとに見やすく色付け(シンタクス・ハイライトや構文ハイライトとも呼ぶ)をしてくれるJavaScriptライブラリである。Prismという名称は、光を通すと七色に分かれるプリズムの性質に由来するものと思われる。

シンタクス・ハイライトに関するライブラリには、既存の静的なテキストを対象にするものと、リアルタイムに入力される動的なテキストを対象にするものがある。Prism.jsは前者であり、テキストエディタのようなリアルタイム入力でのハイライトを実現したいときは、CodeMirrorMonaco Editorといったエディタ・ライブラリが使われる。

※現在Prism.jsのv2の開発が進められているが、本記事で扱うのはv1である。

Prism.jsの基本的な使い方

Prism.jsは、<code>タグに必要なクラス指定を行えば、必要なJavaScriptファイルとCSSファイルを読み込むだけで使える。単純にハイライトするだけなら、APIを使ってこちょこちょコードを書く必要はない。

  1. ハイライトしたいコードを<code>タグで囲ったHTMLファイルを用意する
  2. <code>タグに、language-xxx(またはlang-xxx)という形でクラス指定する。
  3. そのHTMLで所定のCSSファイルとJavaScriptファイルを読み込む

なおハイライトには必須ではないが、<code>タグのみだとレイアウトが難しくなるため、複数行のコードを含む<code>タグは<pre>タグで囲うのが普通である。

preは「preformatted text」の略で、「整形済みテキスト」を意味する。名前の通り、<pre>タグの中では改行などの通常のHTMLタグの中では無視される文字が、そのままの形で表示される。

以下は、Prism.jsを使ってコードをハイライトする最小限のサンプルである。

html
<html>
  <head>
    <meta charset="UTF-8">
    <!-- Prism.jsのテーマを装飾するCSSファイル -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css">
  </head>
  <body>
    <pre><code class="lang-md"># Prism.jsの主なファイル構成

- *テーマ用ファイル* : ハイライト用のスタイルがまとまったCSSファイル。
- *コアファイル* : 各言語に共通する処理がまとまったJSファイル。
- *言語ファイル* : 言語ごとの構文解析ルールがまとまったJSファイル。
- *プラグインファイル* : ハイライト以外の機能を追加するためのJSファイル。

</code></pre>

    <!-- Prism.jsのコアファイル -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js"></script>
    <!-- Prism.jsのオートローダ -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
  </body>
</html>

「コードをハイライト」と言いながら、Markdownで書いてしまった。さておきPrism.jsでハイライトを行うために最低限必要なファイルは、テーマファイル、コアファイル、そして言語ファイルの3つである。

ただ、必要な言語に応じて、いちいちファイルを追加するのは骨が折れる。かといってすべての言語をまとめると重くなる。この課題を解決するために、Prism.jsには自動で必要な言語ファイルをロードするモジュールが用意されている。

テーマファイルとコアファイル、そしてオートローダの3つがあれば、基本的なハイライトが実現できる。

始め方

Prism.jsを利用する方法は、大きく分けて2つある。CDNを利用する方法と、ダウンロードする方法である。

先のサンプルでは、cdnjsというCDNプロバイダを利用した。

CDNはContent Delivery Networkの略で、JavaScriptやCSSなど、Web開発に関連するライブラリを配信するサービスである。代表的なものに、cdnjs、jsDelivr、UNPKGなどがある。CDNの多くは広告収入やスポンサーシップ、寄付金などにより運用されており、無料で利用できる(中には有料のCDNもある)。

利用するには、CDNプロバイダにアクセスし、該当ファイルを検索して、ファイルが見つかったらそのURLを自身のファイルにコピー&ペーストすれば良い。サインアップなどの面倒な手間は一切ない。

ちなみに公式ドキュメントでは、以下のCDNが紹介されている。

cdnjs https://cdnjs.com/libraries/prism
jsDelivr https://www.jsdelivr.com/package/npm/prismjs
UNPKG https://unpkg.com/browse/prismjs@1/

また、ダウンロードに関しては、公式サイトのこちらのページに親切な設計のダウンロードシステムが用意されている。要件に応じてぽちぽちクリックしていけば、最小の構成で必要なファイル群(のバンドルファイル)が手に入る。JSとCSSを1つずつダウンロードし、それを読み込んでおしまいである。

特殊記号のエスケープについて

HTMLのマークアップに使われる記号を文字として表示したい場合、その記号をエスケープする必要がある。これは<pre>タグや<code>タグの中でも同じである。主なところでは、以下のような文字は、エスケープ文字に置き換えねばならない。

エスケープ前 エスケープ後
& &amp;
< &lt;
> &gt;
" &quot;

例えば<span>と表示したい場合、<code>タグの中では&lt;span&gt;としないと、ブラウザは該当の文字列をHTMLタグと認識して処理してしまう。

目に悪い。

ちなみにこういった文字をエンティティ文字という。エンティティは実体という意味で、つまり彼らは世をしのぶ仮の姿で生きているわけである。一皮向けば、かように醜い姿をしているわけだ。怖い。

エンティティ文字の一覧は、WHATWGのこのページにまとられている。

サポートされている言語

公式サイトによると、2023年2月現在297の言語がサポートされている。よほどマニアックな言語でない限り、Prism.jsでシンタクス・ハイライトは賄えるはずである。

また、仮にサポートされていなくとも、正規表現さえ身につけていれば簡単に言語定義を追加できる。具体的な追加方法は、公式ページで紹介されている。

プラグインで機能を追加するには

Prism.jsはコアの機能が絞られているぶん、公式のプラグインが充実している。プラグインもコアファイルと同じく、ファイルを読み込むだけで追加できる。ものによっては追加のコードが必要だが、多くは<pre>タグや<code>タグに所定の属性値を追加する程度で済むはずである。

以下に、いくつかのプラグインの利用例をまとめる。

行頭に番号を表示する

行番号を表示するには、Line Numbersというプラグインを使う。

具体的には、該当のJavaScriptファイルとCSSファイルを読み込んで、行番号をつけたいコードブロックの<pre>タグに「line-numbers」というクラス属性を追加すればよい。

html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css">
    <!-- 行番号用のCSS -->
    <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.css">
  </head>
  <body>
    <pre class="line-numbers">
<code class="lang-pug">h2 コードに行番号を付けるには
ol
  li Line Numbersプラグイン(JSとCSS)を読み込む
  li 行番号をつけたいpreタグのクラス属性に「line-numbers」を指定`</code></pre>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
    <!-- 行番号用のプラグイン(JS) -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.js"></script>
  </body>
</html>

ツールバーを使う

いくつかのプラグインは、ツールバーというパーツの中で実現される。ツールバー自身は、Toolbarというプラグインで追加される。

Toolbarプラグインは、ハイライト対象の<pre>タグを<div class="code-toolbar">というコンテナ要素に格納し、さらに<pre>タグの直後に<div class="toolbar"></div>という要素を追加する。

また、<pre>タグにdata-label属性、あるいはdata-url属性を指定しておくと、ツールバーにその属性値を反映した<span>タグ、あるいは<a>タグがレンダリングされる。複雑な要素を表示したい場合、data-labelに指定した値をidに持つ<template>を用意しておくと、その中身が使われる。

Prism.plugins.toolbar.registerButton()メソッド

Prism.jsは、グローバルに(windowオブジェクトのプロパティに)Prismオブジェクトを追加する。このオブジェクトを通してAPIを操作し、諸々の処理が実現できる。

さらに柔軟な機能をツールバーに追加したいときは、プラグインのjsとcssを読み込んで上で、Prism.plugins.toolbar.registerButton()メソッドを使って任意のボタンを実装する。このメソッドは、以下の2つの引数を取る。

概要
string ボタンを識別するための任意の文字列
ButtonOptions | Function ボタンを生成するためのオプション情報を格納したオブジェクト、あるいは表示したい任意のDOMオブジェクト(<span><a><button>のいずれか)を返す関数。

また、ButtonOptionsは、以下のように型定義されている。

プロパティ名 概要
text ボタンのラベル
url リンクのURL(これを指定すると<button>タグではなく<a>タグになる)
onClick クリックされた場合に反応するイベントリスナ。引数に渡される変数には、
className ボタンに指定するクラス名。

onClickに渡すイベントリスナは、envという変数を受け取る。envは、この文脈では以下のようなプロパティを持つオブジェクトである。

プロパティ名 概要
code <code>タグの中身のテキスト。
element <code>のDOMオブジェクト。
highlightedCode ハイライトされた(<span>タグで装飾後の)コード。
grammer トークンごとの正規表現。
language <code>タグ内のコードの言語。

以下は、ツールバーにボタンを表示して、<code>タグの中身を書き換えるコードである。

html
<html>
<head>
  <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css" rel="stylesheet" />

  <!-- ToolbarプラグインのCSS -->
  <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/toolbar/prism-toolbar.min.css" rel="stylesheet" />
</head>
<body>
  <pre><code class="lang-html"><p>ダラララララ……</p></code></pre>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>

  <!-- ToolbarプラグインのJS -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/toolbar/prism-toolbar.min.js"></script>

  <script>
    Prism.plugins.toolbar.registerButton('ボタンを識別するためのキー', {
      text: 'Click Me!',
      onClick: function (env) {
        env.element.innerHTML = 'だだーん!';
      }
    });
  </script>
</body>
<html>

なお、ツールバーの見た目は、CSSでかんたんに変更できる。

ツールバーに言語名を表示する

ツールバーに言語名を表示するには、Toolbarプラグインを読み込んだ上で、Show Languageというプラグインを読み込む。

html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css" rel="stylesheet" />

    <!-- ToolbarプラグインのCSS -->
    <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/toolbar/prism-toolbar.min.css" rel="stylesheet" />
  </head>
  <body>
    <pre><code class="lang-js">/* このコードブロックをマウスオーバーすると
幽霊のように「JavaScript」の文字が浮かび上がる。 */</code></pre>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>

    <!-- Toolbarプラグインのスクリプト -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/toolbar/prism-toolbar.min.js"></script>

    <!-- Show Languageプラグインのスクリプト -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/show-language/prism-show-language.min.js"></script>
  </body>
</html>

ちなみにShow Languageプラグインを追加すると、ツールバーの中身は以下のようになる(JavaScriptの場合)。

html
<div class="toolbar">
  <div class="toolbar-item"><span>JavaScript</span></div>
</div>

スタイルを変更したいときは、上記のクラス指定を上書きすれば良い。

Prism.jsに機能を追加するには、このようにプラグインを適宜読み込み、必要に応じてスタイリングを変更していく。また、必要な機能が存在しない場合は、Prismというグローバルオブジェクトを通して諸々の操作を行う。

新たな言語定義を追加する

Prism.jsでは、どの言語も正規表現と対応するトークンの集合として定義されている。

そのため正規表現さえ理解していれば、新たに言語定義を追加するのは難しくない。言語定義は、Prism.languagesに、言語名をキーにして保存される。

以下は、.コマンド名 文字列、あるいはPRAGMA 文字列というコードをハイライトする雑な言語定義である。

js
Prism.languages["sqlite3"] = {
  "cmd-param": /(?<=((^|\n)\.\S+\s|(^|\n)PRAGMA)).+/,
  "cmd-name": /(^|\n)(\.\S+|PRAGMA)/,
};

sqlite3は、SQLiteのシェルである。どうやら公式にないので、いつかこのブログでSQLiteのチートシートをまとめる時のためにサンプルにしてみる。

言語を定義する際には、Prism.languages.sqlite3={/*略*/};のように、ドット記法も使える。

ただ、複数の単語から構成される言語名には、キャメルケースではなくケバブケースを使うようである。言語名やトークン名は、そのままクラス名としてHTMLに埋め込まれる。そのため、CSSの命名規則に則したいのではないかと想像する。ブラケット記法を使った方が無難だ。

また、トークンに該当する文字列は、トークンを定義した順に処理(=消費)される。例えば、上記のcmd-paramは、cmd-nameが先にある場合にマッチする。こういった場合、定義の順番を逆にすると機能しない。

以上を踏まえて、独自の言語定義を追加するサンプルを以下に示す。

html
<html>
  <head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css">
    <style>
      /* トークン名と同名のクラスでスタイリング */
      .cmd-name{color:darkmagenta;}
      .cmd-param{color:lightseagreen;}
    </style>
  </head>
  <body>
    <pre><code class="lang-sqlite3">.output path/to/output.txt
PRAGMA table_info(table_name);</code></pre>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js"></script>
    <script>
      /* カスタムの言語定義は、必ずコアファイルを読み込んだ後で読み込む */
      Prism.languages['sqlite3'] = {
        'cmd-param':/(?<=((^|\n)\.\S+\s|(^|\n)PRAGMA)).+/,
        'cmd-name':/(^|\n)(\.\S+|PRAGMA)/
      };
    </script>
  </body>
</html>

より柔軟な定義が必要なときは、トークン名に対して正規表現ではなく、設定オブジェクトを渡す。設定オブジェクトの詳細は、面倒くさいので省略する(公式ドキュメントのここに解説がある)。

言語定義を拡張する

既存の言語定義に、任意の言語定義を後付けして新しい言語を作る場合、Prism.languages.extend()メソッドを使う。このメソッドは、既存の言語定義に影響を与えず、引数に渡した定義を追加した新たな言語定義を返す。

js
/* ※事前に拡張する言語を読み込んでいることが前提 */

Prism.languages["lang-name"] = Prism.languages.extend(
  id /* 拡張する言語のid */,
  redef /* 追加or上書きする定義 */
);

既存の言語定義を変更する場合は、Prism.languages.insertBefore()メソッドを使う。このメソッドは、渡された言語定義に任意の定義を挿入した新しい言語定義を作り、古いものと置き換える。その上で、新しい言語定義を返す。

js
/* ※事前に拡張する言語を読み込んでいることが前提 */

Prism.languages.insertBefore(
  inside, /* 拡張する言語のid */,
  before, /* 定義を挿入するトークン名 */
  insert, /* 挿入する定義 */
  root    /* insideに指定した言語を保持するオブジェクト。デフォルトはPrism.languages */
);

insertBefore()という名称なのに、新しいオブジェクトを作って返すのは変だ。と思ったがそうでもない。最近は変ではなくなってきている。いわゆる「いみゅーたぶる」だ。

ただ、ここではもっと実際的な理由からイミュータブルなアプローチが採用されているらしい。

オブジェクトを直接変更すると、すべてのブラウザで値のイテレーション順が保証されないという問題がある。これに対処するため、新たにオブジェクトを作り、イテレーションで参照をコピーしつつ、該当のトークンの前に渡された定義を挿入している(ソースはここ)。

元の言語を拡張したいが、extend()メソッドでは間に合わないケースもある。追加するトークンの対象の文字列が、既存の定義によってすでに消費されている場合だ。こういうときは、まずextend()メソッドに空の言語定義を渡し、丸ごとコピーした上でinsertBefore()メソッドを使う。

以下は、そのデモである。

html
<html>
  <head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css">
  </head>
  <body>
    <pre><code class="lang-sqlite3">.output path/to/output.txt</code></pre>
    <pre><code class="lang-sqlite3">PRAGMA table_info(table_name);</code></pre>
    <pre><code class="lang-sqlite3">select * from table_name;</code></pre>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
    <script>
      Prism.plugins.autoloader.loadLanguages(['sql'],()=>{
        Prism.languages['sqlite3'] = Prism.languages.extend('sql',{}) ;
        const grammar = {
          'dot-cmd-param':{
            pattern: /(?<=((^|\n)\.\S+\s|(^|\n)PRAGMA)).+/,
            alias: 'string'
          },
          'dot-cmd-name':{
            pattern: /(^|\n)(\.\S+|PRAGMA)/,
            alias: 'keyword'
          },
        };
        Prism.languages.insertBefore('sqlite3', 'keyword', grammar);
        Prism.highlightAll();
      });
    </script>
  </body>
</html>

言語を拡張する際は、その言語がPrism.languagesに含まれている必要がある。CDNを使う場合、コアファイルとは別に拡張対象の言語をCDNで読み込む方法と、Autoloaderプラグインに含まれるloadLanguages()メソッドを使って読み込む方法がある。上記のデモでは、後者の方法を使った。この方法が合っているかは知らない。

loadLanguages()メソッドは、第一引数に読み込む言語のid。第二引数に、言語を読み込んだ後に実行されるコールバックを指定できる。

参考資料