サイボウズ Office on cybozu.com でシンタックスハイライトを実現する方法

「サイボウズ・アドベントカレンダー2012」も今日が最終回(これまでの記事一覧)。トリを務めるのはこの人です。


サイボウズの畑です。

いつもはコード置場で、サイボウズ Office on cybozu.com のJavaScriptによるカスタマイズネタを紹介している私から、掲示板やメッセージでソースコードをシンタックスハイライトする方法を紹介したいと思います。

ブログなどでシンタックスハイライトを実現する方法は各所で紹介されていることと思いますが、今やJavaScriptによるカスタマイズが行えるサイボウズ Office on cybozu.com でもシンタックスハイライトを実現する方法があります。

JavaScriptでシンタックスハイライトを実現するための定番となっているライブラリとして

の2つがあります。今回は読み込むJSファイルの数が少なくて済む google-code-prettify の方を使用した例を紹介します。

今回紹介する内容では、掲示板およびメッセージの本文およびフォローにおいて {code}...{/code} で囲まれた部分をシンタックスハイライトすることにします。(本当はコード部分の自動判別までできると嬉しいのですが、空行とかを考えるとかなり難しそう。)本記事の最後に示すJavaScriptコードをアップロードすることでシンタックスハイライトが実現します。

以下、コードを解説します。

  1. google-code-prettify の読み込み
    まず最初に google-code-prettify のモジュールを読み込む必要があります。CustomizeJS.page にページ名が入るので、掲示板およびメッセージの詳細画面のときのみ読み込みます。今回は js.cybozu.com に置いたモジュールを使用しています。

  2. 前処理を行う関数の定義
    サイボウズ Office on cybozu.com ではあらかじめ jQuery が読み込まれているので、$(document).ready(function () {...}) の中で処理を記述していきます。 google-code-prettify でシンタックスハイライトを行うにはコードの部分を pre タグで囲む必要があります。そして pre タグの class 属性として prettyprint というCSSのクラスを与えます。文章の中からコード部分を見つけ出して pre タグで囲む処理を行うのがこの関数です。

  3. tt タグの中身に対して前処理
    画面内の全ての tt タグの中身が本文およびフォロー文となるので、$('tt') で中身を取り出して前処理を実行します。

  4. google-code-prettify の呼び出し
    前処理でコード部分が見つかれば、prettyPrint() をコールします。 これで、シンタックスハイライトは実現しますが、実はフォローを追記したとき、フォローが再描画されるので、このままだとシンタックスハイライトした変更が失われます。そこで、以下の処理を追加します。

  5. フォロー描画関数の上書き
    かなりハックしないと分かりにくのですが、フォローが書き込まれると、RenderFollows という関数が呼ばれて、画面内のフォローが再描画されます。このときシンタックスハイライトを施した変更も失われるので、再描画の際に再度シンタックスハイライトしてやる必要があります。そこで、RenderFolows という関数を上書きすることによって、これを実現します。

解説は以上となりますが、サイボウズ Office 上でソースコードを使って議論するというのは、かなりマニアックな使い方かもしれませんが、サイボウズ・ラボで普段から行っています。

サイボウズ・ラボからサイボウズ Office の開発チームに対して「シンタックスハイライトを実現してくれ」という要望を出したが「そんな特殊要望は難しい」と切り返された、というやり取りがあったとかなかったとか!? いずれにせよ、JSカスタマイズにより実現できることになったのでハッピーです。


3週間に渡りご愛読いただきましてありがとうございました。また機会があれば、このようなお祭り企画を検討したいと考えています。引き続きCybozu Inside Outをよろしくお願いいたします。


if (CustomizeJS.page == 'BulletinView' || CustomizeJS.page == 'MyFolderMessageView') {
    // (1) google-code-prettify を読み込む
    document.write('<link rel="stylesheet" type="text/css" href="https://js.cybozu.com/prettify/1-Jun-2011/prettify.css" />');
    document.write('<script type="text/javascript" src="https://js.cybozu.com/prettify/1-Jun-2011/prettify.js"></' + 'script>');
}
 
$(document).ready(function () {
    if (CustomizeJS.page == 'BulletinView' || CustomizeJS.page == 'MyFolderMessageView') {
 
        // (2) シンタックスハイライトの前処理を行う関数
        CustomizeJS.prettify = function ($this) {
            // {code}...{/code} の検出
            var html = $this.html();
            if (html.indexOf('{code') < 0 || html.indexOf('{/code}') < 0) return false;
 
            // 文章を1行単位に分割
            var inCode = false;
            var noLF = (html.indexOf("\n") < 0); // ブラウザによって改行の扱いが異なる
            var lines = noLF ? html.split('<BR>') : html.split("\n<br>");
            html = '';
            for (var i = 0; i < lines.length; i++) {
                // 1行ごとの処理
                var line = lines[i];
                if (inCode) {
                    if (line.indexOf('{/code}') >= 0) {
                        // {/code} を </pre> に置換
                        html += line.replace('{/code}', '</pre>');
                        inCode = false;
                    } else {
                        html += line + (noLF ? "\n" : '');
                    }
                } else {
                    if (line.indexOf('{code') >= 0) {
                        // {code} を <pre class="prettiyprint lang-(language)"> に置換
                        html += line.replace(/{code\s*([^}]*)}/, '<pre class="prettyprint lang-$1">');
                        inCode = true;
                    } else {
                        html += line + '<br />';
                    }
                }
            }
            if (inCode) html += '</pre>';
            $this.html(html);
            return true;
        };
 
        var prettify = false;
        $('tt').each(function () {
            // (3) tt タグの中身に対して前処理
            if (CustomizeJS.prettify($(this))) prettify = true;
        });
 
        if (prettify) {
            // (4) google-code-prettify の呼び出し
            prettyPrint();
            $('pre.prettyprint').css('padding', '0.5em').css('background', '#fff'); // CSSを調整
        }
 
        // (5) フォロー描画関数の上書き
        window.RenderFollowsOrig = window.RenderFollows;
        window.RenderFollows = function (append, dummy) {
            window.RenderFollowsOrig(append, dummy);
 
            var prettify = false;
            var $follows = $('.vr_followList:last tt').each(function () {
                if (CustomizeJS.prettify($(this))) prettify = true;
            });
 
            if (prettify) {
                prettyPrint();
                $follows.find('pre.prettyprint').css('padding', '0.5em').css('background', '#fff');
            }
        }
    }
});

【修正履歴】 2012年12月21日:文章末の編集者コメントの後に(コードと区別して見やすくするために)罫線を引きました。