CSS::LESS::Filter で Twitter Bootstrap をいい感じに改造しよう
みなさんさんざん苦労するサンタクロース業務の準備は進んでいますか? うちでもいま某案件のクルシミマスリリースに向けて現在見栄えをよくする作業にいそしんでいるのですが、今回採用した Twitter Bootstrap が案外融通のきかないヤツだというのがわかったので、なんとかするべく CSS::LESS::Filter というのを書いてみました。
基本的な使い方はこんな感じ。github から Bootstrap を clone してきて、このスクリプトを実行すると、breadcrumbs.less と variables.less を書き換えて背景色を指定できるようになります。
#!/usr/bin/env perl
use strict;
use warnings;
use CSS::LESS::Filter;
use Path::Extended;
# breadcrumbs の背景色が固定値なのはトンマナ崩れるので困ります!
{
  my $filter = CSS::LESS::Filter->new;
  $filter->add('.breadcrumb { background-color:' => '@breadcrumbBackground');
  my $file = file('less/breadcrumbs.less');
  my $less = $file->slurp;
  $file->save($filter->process($less));
}
# 変数の値も変更するよ(この場合はないから追加ね)
{
  my $filter = CSS::LESS::Filter->new;
  $filter->add('@breadcrumbBackground:' => '#eef8ff');
  my $file = file('less/variables.less');
  my $less = $file->slurp;
  $file->save($filter->process($less, {mode => 'append'}));
}
ここではわかりやすく単純な変数や定数へ置き換えてみましたが、もちろんトンマナ維持のためには変数はなるべく少なくおさえて、LESS の各種関数で色味などの調整をした方がよいでしょう。CSS::LESS::Filter は、内部的には Parse::RecDescent を使って結構マジメに LESS の解析を行っていますが、ユーザレベルでは必要な値をまるごと文字列としてやりとりするようになっているので、LESS の関数だろうとなんだろうと、そのまま渡せます。
{
  my $filter = CSS::LESS::Filter->new;
  $filter->add('.breadcrumb { background-color:' => 'lighten(@navbarBackground, 30%)');
  my $file = file('less/breadcrumbs.less');
  my $less = $file->slurp;
  $file->save($filter->process($less));
}
セレクタは、基本的には { でつないで、プロパティの場合は最後に : をつけて指定しますが、セレクタが複雑でいちいち全部書いていられないという場合は、正規表現を使って指定することもできます。
  # もっと長いのもありますが、これでも十分長いですよね!
  my $selector = '.tooltip { &.top .tooltip-arrow { left:';
  $filter->add($selector => '40%');
  # こう書いてもOK
  $filter->add(qr/\.top \.tooltip-arrow \{ left:/ => '40%');
ただし、正規表現を使った書き方は、マッチするセレクタがない場合、(セレクタを一意に決められないので)ファイル末尾に該当のデータを追加する機能が利用できないという制約があります。また、うっかりすると想定外のところが書き換えられる可能性もありますので、利用の際にはご注意ください。
ルールセットにプロパティやルールを追加したい場合は、このようにすると該当部分がまるごと文字列として渡ってきますので、前でも後でも好きなものを好きなように追加してください。返した値がそのままルールセットのブレースの中身になります。
  # ルールセットを指定する場合は最後を { で止めます。
  $filter->add('.breadcrumb {' => sub {
    my $inside = shift;
    $inside .= "  color: \@navbarLinkColor;\n";
    $inside;
  });
ルールセットやプロパティをまるごと削除したい場合、上のように上位のルールセットの中身をまるごと取ってきて正規表現などで消してもよいのですが、undef を渡すとセレクタごと簡単に消してしまえます。
  $filter->add('.breadcrumb { .divider {' => undef);
@import などのルールも操作できますが、これらはうかつに定数値を渡すと、同じコンテキストに複数回出てきたときに一律同じ値が適用されてしまうので、コールバックの中で値を見ながら操作するのが安全です。
  $filter->add('@import' => sub {
    my $value = shift;
    # アコーディオンやカルーセルは使わないので消しちゃいましょう!
    return if $value =~ /(accordion|carousel)\.less/;
    return $value; # あとのは現状維持で
  });
@import したいファイルが増えた場合などはセレクタとして空文字を渡すと、ファイルの末尾に任意のルールを追加できます。
  $filter->add('' => sub {
    return <<'LESS';
@import "foo.less";
@import "bar.less";
LESS
  });
現状で、Twitter Bootstrap はもちろんのこと、less.js のテストに使われているかなり意地悪な CSS/CSS3 を含むファイルもすべて要素を落とさずにパースできることは確認済みですが、正しくパースできないファイルがあるとか、ほかにもこんな要素にフックをかけられるとうれしい等々の問題や要望があったらお知らせください。
なお、CSS::LESS::Filter は、当然と言えば当然ですが LESS ではない素の CSS もふつうに扱えます。ただ、LESS から CSS への変換には対応していないので、CSS::LESS::Filter で Bootstrap を改造したあとは手順通りに recess などを走らせてください。最近は Windows 環境でもふつうに動きますので、CSS::LESSp などでがんばるより楽だとおもいます。
さて、明日はどなたかな?