highlight.js で独自言語の色付けを追加

Page content

LuneScript の解説サイトは、 hugo を使用して構築している。 その解説サイトに掲載しているソースコードは、 hugo によって解析されて、色付けに必要な <span class=""> が付加され、 css で色付けを行なっている。 なお、 ソースコードの解析自体は hugo というよりも、 hugo が chroma の API を呼び出して利用している。

しかし LuneScript は超マイナー言語なので、 chroma の対応言語には当然 LuneScript が入っていない。

これだと LuneScript のサンプルコードのハイライトが付かないため、コードを読み難い。 そこで、LuneScript のハイライト表示に対応するために highlight.js を導入したので、 今回は highlight.js を利用して独自言語の色付けを行なうための方法を簡単に説明する。

ハイライト表示の対応手段として chroma の方を変更するという方法もあるが、 highlight.js の方が hugo を使用していないどの Web サイトでも使えるので汎用的だろう。

ちなみに chroma は、 hugo で静的サイトを構築する際に コンテンツ内のソースコードを解析して、解析結果を反映した html を出力する。 一方で highlight.js は、 Web ブラウザでソースを表示する際に動的にソースコードを解析する。

つまり、 chroma 側で対応した方がブラウザの負担を減らし UX を向上できる。 しかし、サンプル程度の短いソースコード解析であれば、 さほど解析に時間がかかることもないので、気にする必要はないだろう。

highlight.js への独自言語追加

まずは以下を追加する。

 <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>

なお、 highlight.js の公式サイトには default.min.css のロードの記載もあるが、 独自言語追加には不要である。

highlight.js によるソースコード解析

次の関数を実行して、ソースコードを保持する element を highlight.js で解析する。

hljs.highlightBlock( element );

このとき element の class は、 language-言語識別 として定義しておく。 例えば LuneScript は language-lns としている。

なお、 highlight.js の使用方法として次の関数を実行する方法が紹介されているが、

hljs.initHighlightingOnLoad();

この関数は全ての <pre><code> element を解析対象としてしまう。

今回は、 LuneScript 以外の言語を hugo で解析済みなので、 全ての <pre><code> を解析対象にしてしまうと 2 重解析になってしまうため、 hljs.highlightBlock( element ); で解析する element を明示的に指定する。

hljs.highlightBlock( element ); によって highlight.js による解析が行なわれるが、 まだこの状態では highlight.js は独自言語に対応していない。 そこで、highlight.js に独自言語の情報を事前に登録しておく。

highlight.js への独自言語の登録

highlight.js へ独自言語を登録するには次の関数を利用する。

hljs.registerLanguage( langName, langDef )

ここで、 langName は前述の language-言語識別言語識別 部分を指す。 つまり language-lns の場合 "lns" を langName に使用する。 langDef は、次のような関数オブジェクトを指定する。

function( obj ) {
   return {
      keywords: "hoge foo bar"
   };
}

つまり、まとめると以下のようになる。

hljs.registerLanguage( "lns", function( obj ) {
   return {
      keywords: "hoge foo bar"
   };
});

上記の langDef で定義する関数オブジェクトは、 言語情報を定義するオブジェクトを返す。

このオブジェクトの詳細は次の URL に記載がある。

<https://highlightjs.readthedocs.io/en/latest/mode-reference.html>

以降では、良く使う属性について説明する。

言語情報定義オブジェクト

まず、言語情報定義オブジェクトが何を定義するものかを説明する。

highlight.js は、ソースコード内の文字列を解析し、 「どの文字列」が「何の種別」かを判別する。

このオブジェクトは、「どの文字列」「何の種別」を定義するのが役割である。

たとえば、 C 言語では for, while, if などの文字列の種別は予約語(keyword) であり、 /* */ で括られている文字列の種別はコメント(comment) である。

次のオブジェクトを返すことで、for, while, if を keyword として定義できる。

   return {
      contains: [
        {
	    className: "keyword",
	    keyword: "for while if"
        }
      ]
   };

ここで className は、 for while if が keyword であることを示す。

この定義のよって、 highlight.js は解析対象のソースコード内の for を、次のように変換する。

<span class="hljs-keyword">for<span/>

highlight.js は、上記オブジェクトの className で指定した名前を span element のクラス名として使用する。

この例の場合 className: "keyword" で定義したクラス名は、 "hljs-keyword" となる。 仮に className が "hoge" ならば、 "hljs-hoge" となる。

このように 言語情報オブジェクトで定義した各文字列にクラスが指定されるので、 CSS によって hljs-keyword に色を指定することでソースコードの色付けが可能になる。

なお、 className は任意の文字列を定義可能だが、 もし将来独自言語の対応を highlight.js に pull request したい、 という思いがあるならば、 highlight が既に対応している言語に合せて className を利用するべきだろう。

contains

{
   contains: [
      { className: "keyword", begin: /hoge|foo|bar/ }
   ]
}

contains は、 sub-mode を配列で指定するためのものである。 sub-mode は JavaScript の object で、 上記の例では { className: "keyword", begin: /hoge|foo|bar/ } が sub-mode である。 複数の種別を定義する際に利用する。

begin, end

begin は、定義する種別の文字列の開始パターンを定義する。 なお、 end を明示的に指定しない場合、 begin でマッチした文字列だけが、所定の種別になる。

つまり、 { className: "keyword", begin: /hoge|foo|bar/ } は、 種別 keyword は、文字列 hoge , foo, bar から成ることを定義している。

もしも end に end: /$/ を指定した場合、 hoge, foo, bar のいずれから始まり、その行末までが指定した種別 keyword になる。

ネスト

sub-mode はネストできる。

{
   contains: [
       {
           className: "keyword",
           begin: /abc/,
           end: /ij/,
           contains: [ 
               { className: "meta", begin: /ef/ }
           ]
       }
   ]
}

上記は keyword の種別の中に meta を含む定義である。

これは、次のような文字列があった場合、

abc defgh ijk

abc 〜 ij までを "keyword" として扱い、 その中の ef を "meta" として扱う。

この時の HTML 出力は次になる。

<span class="hljs-keyword">abc d<span class="hljs-meta">ef</span>gh ij</span>k

ネストすることで、ある種別の中に別の種別を定義することが可能になる。

LuneScript の highlight.js 設定

参考までに、 highlight.js に LuneScript を追加登録するスクリプトを載せておく。

<script src="https://ifritjp.github.io/documents/js/highlight_lns.js"></script>
<link rel="stylesheet" href="https://ifritjp.github.io/documents/css/highlight_lns.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>

LuneScript のソースを保持する element の class は、 language-lns として指定する必要がある。