SyntaxHighlighter.jsからhighlight.jsへ移行しました

はじめに

当ブログでは、ブログ上のソースコードをハイライト表示するライブラリを利用しています。ソースコードには、予約後や文字列、数値など予め予測できる書式が多数存在します。それらを指定の色や書体によりハイライト表示することで、ソースコードを読みやす表示しています。

これまで

CDN上のSyntaxHighlighter.jsv3を遅延読み込みして利用していました。

移行理由

次の理由からSyntaxHighlighter.jsからの移行を決意しました。

  • SyntaxHighlighter.jsv3の問題
    • shCore.jsとそれ以外の読込み順序を意識する必要がある
    • shAutoloader.jsで順序を意識する必要はなくなる
      • ただし、shAutoloader.jsが意識的に処理してくれるだけで、順読込みは必須
      • shAutoloader.jsは、指定スタイルをすべて読みこんだり、頭が悪い
      • shAutoloader.js分読込みファイル数が増える
      • 上記理由からshAutoloader.jsは使用したくない
    • 自作の遅延ローダーを使用する
      • ただし、順序を意識するとコード量が増える
      • shAutoloader.jsと大差ないコード量になる
    • 空白行にスペースを挿入する謎仕様がある(バグ?)
      • 自作の遅延ローダーで回避策を実装済み
  • SyntaxHighlighter.jsv4の問題

選定

次のものが選定に名前の上がったハイライト表示用のスクリプトです。制定で特に意識したのは、GitHubのスター量と最終更新日です。スター量を使用人数と仮定しています。最終更新日を今後の問題対処速度と対応速度(予約語の追加速度等)と仮定しています。その結果、候補はhighlight.js、次点でcode-prettifyとなりました。

syntaxhighlighter


highlight.jsは、簡易なハイライト表示ライブラリです。

コア部分は、12KB程度です。それに加えて各言語用設定とCSSのスタイル設定が必要になります。各言語用設定は、150言語以上をサポートしており、一般的な言語は全て貰うされています。HTML上では、コア部分と各言語設定をパックしたJavaScriptファイルを使用して読込み回数を削減できます。CDN等で配布されているコードは、すべての言語を含むため、コード量が大幅に増加しています。CSSのスタイル設定は、ハイライトする色を指定する程度で2KB程度の少量です。既存のファイルから変更すれば自作するのも簡単です。

単純に入力を出力する使用方法も想定されているため、サーバー側で事前にハイライト表示処理をすることもできます。クライアント側でのスクリプト実行を不要にできます。また、Web Workersでページのメイン処理から切り離して処理することもできます。

ライセンスは、BSD 3-Clause Licenseです。ライセンスの概要は、義務「著作権情報の表示」「許諾表示を残す」、 禁止「組織・著作権者の名前と貢献者の名前を使わない(書面による特別の許可があれば可能)」、その他「無保証」です。基本的には、ソース上部のライセンスへのリンクを残せば、商用利用・改変・再配布などが可能です。

highlight.jsに問題があるとすればそれは、簡易であるため行番号やタイトルなどの補助的な機能が存在しないことです。ですが、ハイライト表示に補助機能は不要と言う設計思想が透けて見えます。ブログ等で簡易に表示するのであれば補助機能は不要、「補助機能が必要な量のソースコードを表示する必要があるならば、GitHub等の外部サービスを使用する」と考えれば、問題ないように筆者は考えました。

※自動的に無指定の<pre><code>内を処理する仕様であるため、意図しないハイライトが発生する可能性があります。ですが、無指定のハイライトを無効する方法が準備されています。(例:hljs.configure({languages:[]});


code-prettifyは、Google製のハイライト表示ライブラリです。既に筆者の心がhighlight.jsへ揺らいでいるため、簡易な選定となります。

code-prettifyは、highlight.jsと異なり行番号を含むフル機能を提供します。それに伴い、ソースコード量も増加します。ですが、ローダーが準備されており、<pre>への設定をしておけばJavaScript側をユーザが意識することはなさそうです。言語は、38種類をサポートしており基本的な言語は網羅しています。

採用

highlight.jsを採用します。

採用の決め手となった理由は、行番号などの補助機能を実装しない設計思想です。筆者には、これまで行番号は必要と言う固定観念がありました。ですが、それが不要と言う考えに感銘を受けたため、採用しました。

(行番号が必要だと考えるのであれば、code-prettifyを採用しておけば良いと思います。code-prettifyでサポートされていない言語を使用するならば、Prismを採用すると良いと思います)

導入

highlight.js読込み用のスクリプトローダーを作成しました。
機能概要は、次の通りです。

  • SyntaxHighlighter.js用の<pre>記載をhighlight.js用に置き換え
  • ハイライト表示なしの場合、highlight.jsを読込まない
  • ハイライト指定がない場合、ハイライトしない
  • highlight.jsの遅延読込み
  • タイトル表示機能を追加
    • 補助機能を実装しないとは…

コードは、次の通りです。

タイトル用.csspre {
  background: #2D2D2D;          /* hljsの設定次第 */
}

.pre-code-title {
  display: inline-block;
  margin: 0 1em;                /* hljsの設定次第 */
  padding: 2px 8px;
  background: #777;             /* hljsの設定次第 */
  color: #EEE;                  /* hljsの設定次第 */
  word-break: break-all;
}
HighlightLoader.js(function(document) {
  const main = function() {
    // SyntaxHighlighter.jsを置き換え
    const brushs = document.querySelectorAll('pre[class^="brush:"]');
    if (brushs.length) {
      const re = /brush:\s*([^\s;]+)\s*;?/;
      for (let i=0; i<brushs.length; i++) {
        const m = brushs[i].className.match(re);
        if (m != null) {
          const code = document.createElement('code');
          code.className = 'language-'+m[1]+(brushs[i].title ? ':'+brushs[i].title : '');
          brushs[i].className = '';
          brushs[i].insertBefore(code, brushs[i].firstChild);
          for (let next; next = code.nextSibling; ) {
            code.appendChild(next);
          }
        }
      }
    }

    const codes0 = document.querySelectorAll('pre>code[class^="language-"]');
    const codes = Array.prototype.filter.call(codes0, function(code) {
      return !code.classList.contains('hljs');
    });
    if (codes0.length) {
      // css読込み
      const style = document.createElement('link');
      style.rel = 'stylesheet';
      style.type = 'text/css';
      style.href = 'スタイルシートURL';
      document.head.appendChild(style);
    }
    if (codes.length) {
      // js読込み
      const script = document.createElement('script');
      script.type = 'text/javascript';
      script.async = true;
      script.addEventListener('load', function() {
        hljs.configure({languages:[]});

        for (let i=0; i<codes.length; i++) {
          hljs.highlightBlock(codes[i]);

          // タイトル表示
          const m = codes[i].className.match(/language-\w+:([^\s;]+)/);
          if (m != null && !codes[i].parentElement.querySelector('.pre-code-title')) {
            codes[i].insertAdjacentHTML('beforebegin', '<span class="pre-code-title">'+m[1]+'</span>');
          }
        }
      });
      script.src = 'スクリプトURL';
      const sc = document.getElementsByTagName('script')[0];
      sc.parentNode.insertBefore(script, sc);
    }
  };

  main();
  //window.addEventListener('lazy', main);
})(document);