Ruby で Sass のカスタム関数を書く

Sass で関数を書く方法は 2 つあります。ひとつは Sass の @function ディレクティブ を使って Sass ファイル内に定義する方法。

$grid-width: 40px;
$gutter-width: 10px;

@function grid-width ($n) {
    @return $n * $grid-width + ($n - 1) * $gutter-width;
}

簡単かつカジュアルに書ける反面、当然ながら Sass のネイティブ関数やディレクティブの制限を受けるので、できないことも多いです。とくに Sass は文字列系の関数があまり用意されておらず、不満を感じることも少なくありません。

もうひとつの方法は、Ruby でカスタム関数を書いて Sass を拡張する という方法です。Ruby の知識が必要で、かつ Sass ファイルに直接定義するのとは違って別ファイルから読み込む必要があったり、少しハードルが高いのも確かですが、Sass だけでは難しい複雑な処理を書けます。

(ちなみに、以前このブログの記事で @function による関数を「カスタム関数」と書いたことがありましたが、おそらくそっちはただの関数で、Ruby の方をカスタム関数と呼んだ方が良さそうです)

というわけで、Ruby は『初めての Ruby』(おすすめ!) をざっと眺めた程度の理解しかありませんが、カスタム関数を書いてみました。

今回書いたのは escape() という関数です。おもに content プロパティで使うことを想定していて、文字列を渡すと \ と Unicode のコードポイントでエスケープしてくれる、というものです (ちなみに CSS における文字のエスケープについては CSS character escape sequences · Mathias Bynens が良くまとまってます)。たとえば、

.hello {
    &:before {
        content: escape("ヾ(๑╹◡╹)ノ");
    }
}

これが、

.hello:before {
    content: "\30FE\28\E51\2579\25E1\2579\29\FF89";
}

こうなります。

これを Sass の @function でやろうとすると、現状ではおそらく巨大な Unicode のテーブルを擬似連想配列っぽいリストに入れて回す、みたいなことになって、あまり実用的じゃありません。やはりここは Ruby でいきたい。そこで、P4D という、プログラマーとデザイナーが集まって何か作ったり相談したりする会があるんですが、そこにお邪魔してルビィストの @satococoa にこういうの作りたいんですけど… とぶつけ、マンツーマンで教えていただきつつ出来上がった (というかほぼ @satococoa が書いた) のがこれです:

module Sass::Script::Functions

    def escape(string)
        assert_type string, :String
        Sass::Script::String.new(string.value.codepoints.map{ |i|
            '\\' + i.to_s(16).upcase
        }.join(''), :string)
    end
    declare :escape, :args => [:string]

end

コードは Sass のドキュメンテーションにある例をもとにしました。

はまったのが string.value の部分で、この .value を抜かして string.codepoints としても動きません。Sass のドキュメンテーション にあるように、String#value メソッドによって文字列が Ruby でいじれるかたちになるらしいです。

あと Ruby の String#codepoints を使っているため、Ruby 1.8 系統では動きません。Mac の Ruby はいまも 1.8 がデフォルトらしいので注意。僕も Qiita の記事 を見ながら 1.9.3 にしました。

Sass::Script::String.new() に渡してる 2 つめの引数 :stringquote() 関数のソースにあったのをそのまま持ってきたもので、文字列が " で囲まれて返ってくるみたいです。

で、使い方。まず上記 Ruby コードを functions.rb などとして、プロジェクトの適当なディレクトリに置きます。そして watch するときに、

$ cd /Users/me/Projects/my-project
$ sass --watch ./sass:./css -t compact -r ./sass/functions.rb

というように、-r オプション (--require のショートカット) でファイルへのパスを渡します。こうするとそのファイルを読み込んでくれて、あとは Sass ファイルのどこでも escape() 関数が使えるようになります。ちなみに、パスの先頭の ./ を抜いてディレクトリ名から指定するとなぜかだめっぽいです。

さらに別のカスタム関数があれば、この module Sass::Script::Functionsend の間に突っ込んでいけばそれぞれ動くみたいです (「みたいです」とか「らしいです」とか「っぽいです」とかばっかりで申し訳ないですが…)。

module Sass::Script::Functions

    def escape(string)
    # ...

    def yet_another_awesome_function(number)
    # ...

end

こんな感じで便利関数をまとめておけばプロジェクト間で使いまわすことも簡単です。この functions.rb を、もし中身が空っぽでもプロジェクトごとの Sass ディレクトリに必ず置き、watch 時には require するようにする、という運用にしてもいいかなと思ってます。扱いとしては WordPress の functions.php みたいな感じ。

Ruby のパワーを借りればほんとに色んなことができそうなので、夢が広がる一方、どこまでやっていいんだろうとか、運用がちょっと面倒くさそうとか、バグっても直せなくて泣いちゃうかもとか、素直に Compass 使えやとか、色々と懸念もありますが。

あと、じつは escape() 関数は 似たようなプラグイン がすでにあったんだけど、そちらは引数の 1 文字目しか見てないのでちょっと違う。やっぱこの手のはマルチバイト圏に暮らす僕たちがこそ頑張りたいですねー。

ヾ(๑╹◡╹)ノ Happy styling!