gist.github.com のリビジョン・インジケータっぽいもの

gist の履歴に、赤色と緑色のドットを並べて、テキストの変更度合いを表したインジケータがついています。あのインジケータは好きなので、Wiki の差分表示ページで似たものを描く方法を考えてみました。

まず、差分をとるページの内容が必要です。

CREATE TABLE pages (
    rev INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT
   ,title VARCHAR(255) NOT NULL
   ,content TEXT NOT NULL
);
CREATE UNIQUE INDEX page_history ON pages(title, rev);

上のような単純なテーブル構成を考えることにします。テーブル通しでリビジョン番号をふって、同じタイトルのある行が編集履歴の集合になっています。テーブルに必要な行を sqlite3 コマンドで手動で insert しておいたとして、それからインジケータを作ります。あるリビジョンと直前のリビジョンのページをとってきます。

use strict;
use warnings;
use Encode;
use DBI;

my $dbh = DBI->connect('dbi:SQLite:dbname=prototyping.db', q(), q(), {
    'PrintError' => 1, 'RaiseError' => 1,
});
my($rev1, $title1, $content1) = $dbh->selectrow_array(<<'EOS', undef, $rev);
SELECT rev, title, content
  FROM pages
  WHERE rev=?;
EOS
my($rev0, $title0, $content0) = $dbh->selectrow_array(<<'EOS', undef, $title1, $rev1);
SELECT P.rev, P.title, P.content
  FROM pages AS P
  WHERE P.title=?
    AND P.rev=(SELECT MAX(H.rev) FROM pages AS H WHERE H.title=P.title AND H.rev<?);
EOS
for ($title1, $content1, $title0, $content0) {
    $_ = decode_utf8($_);
}

これで2つのページの内容が手に入りました。content0 から content1 への変化の差分をとります。差分は Text-Diff3 内蔵の diff を使うことにします。

use Text::Diff3 qw(diff);

my $s1 = [split /\n/msx, $content1];
my $s0 = [split /\n/msx, $content0];

my $change_list = diff($s0, $s1);

diff の出力は、挿入(a)、削除(d)、変更(c) の差分情報からなっています。これから挿入行数と削除行数を集計します。

my $ins = my $del = 0;
for my $r (@{$change_list}) {
    if ($r->[0] ne 'd') {
        $ins += $r->[4] - $r->[3] + 1;
    }
    if ($r->[0] ne 'a') {
        $del += $r->[2] - $r->[1] + 1;
    }
}

集計したそれぞれの行数からインジケータを作ります。

  • 0 < 挿入行数 < 10 のとき、緑ドット1つ
  • 10 <= 挿入行数 < 100 のとき、緑ドット2つ
  • 100 <= 挿入行数 のとき、緑ドット3つ
  • 0 < 削除行数 < 10 のとき、赤ドット1つ
  • 10 <= 削除行数 のとき、赤ドット2つ

ドットの数が 5 個よりも少ないときは灰ドットで埋めます。
テンプレートに Text-Liq を使うことにします。

use Text::Liq;

use utf8;
my $liquid_source = <<'EOS';
<abbr title="{{ins}} 行挿入 &amp; {{del}} 行削除"
><span style="color: #00bb00"
>{%  if ins > 0  %}■{% endif %}{% if ins >= 10 %}■{% endif %}{% if ins >= 100 %}■{% endif %}</span
><span style="color: red"
>{%  if del > 0  %}■{% endif %}{% if del >= 10 %}■{% endif %}</span
><span style="color: #999999"
>{%  if ins <= 0 %}■{% endif %}{% if ins <  10 %}■{% endif %}{% if ins < 100 %}■{% endif
%}{% if del <= 0 %}■{% endif %}{% if del <  10 %}■{% endif %}</span
></abbr>
EOS
my $template = Text::Liq->parse($liquid_source);
my $html = Text::Liq->render($template, {'ins' => $ins, 'del' => $del});
print encode_utf8($html);