JavaScriptでSassのカスタム関数を作る

Sassカスタム関数
--------------

元々の[Sass][]はRubyで書かれていて、Rubyで拡張することで追加の関数を作ることができます。これは[カスタム関数][custom functions]と呼ばれています（`@function`ディレクティブを使ってSassの文法だけで関数を定義する[関数ディレクティブ][function dierctive]とは別です）。

例えば[Compass][]というSassミックスイン集＋αみたいなツールがあって、これを使うと

```scss
.book-cover {
  width: image-width("path/to/image");
  max-width: 36px;
}
```

と書くことで、コンパイル時に画像ファイルの横幅を取得し、それを設定することができます。

ところが、最近は多くの環境がそうだと思うのですが、[gulp][]などでSassを使うために[`node-sass`][node-sass]を使っていると、当然ながらRubyでカスタム関数を書くというわけにはいきません。のでCompassの便利関数なども使えなくなってしまっています。

（余談ですがSassの文法のみで定義されたミックスインは、[compass-mixins][]というNPMパッケージで使うことができます。）

これはnode-sass（などの[libsass][]バインディング達）への移行に踏み切りにくい、また移行しても不満を覚えるところでした。ところが、実はnode-sassのバージョン3から、JavaScriptでカスタム関数を書けるようになっていました： [functions][node-sass-functions]

[Sass]: http://sass-lang.com/
[custom functions]: http://sass-lang.com/documentation/Sass/Script/Functions.html#adding_custom_functions
[function dierctive]: http://sass-lang.com/documentation/file.SASS_REFERENCE.html#function_directives
[Compass]: http://compass-style.org/
[gulp]: http://gulpjs.com/
[node-sass]: https://www.npmjs.com/package/node-sass
[compass-mixins]: https://www.npmjs.com/package/compass-mixins
[libsass]: http://libsass.org/
[node-sass-functions]: https://www.npmjs.com/package/node-sass#functions-v3-0-0-experimental

JavaScript API
--------------

試しに「画像などのファイル名を与えたら、ファイル名にハッシュ値を付ける」という`image-url`関数を作ってみましょう。

```scss
h1.site-name {
  background: image-url("/assets/images/logo.svg") no-repeat;
  // ...
}
```

と書いたら、

```css
h1.site-name {
  background: url("/assets/images/logo-d8913881ba8b302dc6289957112e64f0.svg") no-repeat;
  // ...
}
```

というCSSにコンパイルする関数です。

`/assets/images/logo.svg`というファイルを`/assets/images/logo-d8913881ba8b302dc6289957112e64f0.svg`というファイル名に変更する処理は既にやっておいているものとします（[gulp-rev][]などを使うのがいいでしょう）。

CloudFrontのキャッシュ削除のタイムラグを回避したりするために使えます。

[gulp-rev]: https://www.npmjs.com/package/gulp-rev

### 使う関数の指定 ###

JavaScriptからカスタム関数を使うには、`render`または`renderSync`関数に`functions`というオプションを渡します。

```javascript
var sass = require("node-sass");

var compiled = sass.renderSync({
  file: "path/to/style.scss",
  outputStyle: "expanded",
  functions: {
    "image-url($url)": imageUrl
  }
});
var cssString = compiled.css.toString();
console.log(cssString);
// h1.site-name {
//  background: url("/assets/images/logo-d8913881ba8b302dc6289957112e64f0.svg") no-repeat;
// ...
// }
```

`functions`オプションはオブジェクトになっていて、プロパティ名がSass関数としてのシグニチャー（名前と引数。オプション引数のデフォルト値なども指定可）、値が関数の実体です。同じ関数でも、プロパティ名を変えることで別の関数にすることができます。例えば、ここでは`image-url`という名前にしていますが、やることはフォントファイルでも変わらないので、`font-url`という名前で同じ関数を使い回してもいいでしょう。

### JavaScriptで関数の実体を書く ###

`functions`オプションで指定した関数には、定義したシグニチャーの通りの引数が渡ってきます。

```javascript
function imageUrl(url) {
// ...
}
```

ここで`url`引数は、Sass内で`image-url`に渡した文字列**ではなくて**、渡した文字列を表す**特別なSassのオブジェクト（`SassString`）**です。`getValue`関数を呼び出すことでJavaScriptの文字列にすることができます。

```javascript
function imageUrl(url) {
  var urlValue = url.getValue();
  // ...
}
```

戻り値も、JavaScriptの文字列（や、用途に応じて数値など）ではなくてSass用のオブジェクトに変換しておく必要があります（下の`sass.types.String()`のところ）。

```javascript
function imageUrl(url) {
  var urlValue = url.getValue();
  // ...
  var returnString = 'url(" + urlValue + '")';

  return sass.types.String(returnString);
}
```

（使えるSassの型一覧は[node-sass-functions][]にあります。）

これで`image-url("/assets/images/logo.svg")`を`url("/assets/images/logo.svg")`に置き換えるだけのSassカスタム関数が出来ました。あとは通常のJavaScriptプログラミングで、途中を埋めていくだけなので省略します。

```javascript
var fs = require("fs");
var path = require("path");
var crypto = require("crypto");
var sass = require("node-sass");

var baseDir = "path/to/dir";

function imageUrl(url) {
  var urlString = url.getValue();
  var parsedUrl = path.parse(urlString);
  var realPath = fs.realpathSync(path.join(baseDir, url));
  var content = fs.readFileSync(realPath);
  var hash = crypto.createHash("md5").update(content).digest("hex");
  parsedUrl.name += "-" + hash;
  parsedUrl.base = parsedUrl.name + parsedUrl.ext;
  var returnString = 'url("' + path.format(parsedUrl) + '")';

  return sass.types.String(returnString);
}
```

（`baseDir`が固定とか、ハッシュが長過ぎるとか、毎回画像の中身を読み込んでいるのやばいとか、同期API使ってるとか、とか、記事の趣旨には影響しない所は適当にやっているのが色々あるのでそのままは使わないでください。実際は[gulp-rev][]とかでファイル名変更のテーブルが出来ていると思うので、gulpのストリーム経由で渡してそれを参照するのがいいと思います。）

これで実際に、SassファイルやSCSSファイル中で`image-url`関数を使うことができるようになります。

`@function`ディレクティブを使った関数定義では、ハッシュ値の計算などは（大変そうだけど）できるかも知れませんが、「ファイルを読み込む」というところができないはずです。これがnode-sassバージョン3から入ったカスタム関数の力です。

### 非同期の場合 ###

`sass.renderSync`で呼ぶ時は、関数からSass用のオブジェクトを返すだけでよかったですが、`sass.render`で非同期に呼ばれる場合には別の方法が必要です。

関数の**最後の引数**として完了報告用の関数が渡されるので、それを呼びます。

```javascript
function imageUrl(url, done) {
  var urlString = url.getValue();
  // ...

  done(sass.types.String(returnString));
}
```

`renderSync`の場合には関数になってないので、条件分岐する必要があるでしょう。

```javascript
function imageUrl(url, done) {
  var urlString = url.getValue();
  // ...

  var replacement = sass.types.String(returnString);

  if (typeof done === "function") {
    done(replacement);
  } else {
    return replacement;
  }
}
```

コマンドライン
------------

コマンドラインのインターフェイスをもあります。node-sassパッケージについてくる`node-sass`コマンドに`functions`オプションを渡すことで、自作の関数を有効化した上でSassファイルを変換できます。

```
$ $(npm bin)/node-sass --functions=image-url.js ./app/assets/stylesheets/common.scss
h1.site-name {
  background: url("/assets/images/logo-d8913881ba8b302dc6289957112e64f0.svg") no-repeat;
  // ...
}

```

ここで`functions`オプションに渡す`image-url.js`ファイルはこんな風になっています。

```javascript
functions imageUrl(url) {
// ...
}

module.exports = {
  "image-url($url)": imageUrl
};
```

`sass.render`（`sass.renderSync`）の`functions`オプションに渡していたのと同じオブジェクトを、今度は`module.exports`として設定します。

そういう機会があるかちょっと分かりませんが、これでコマンドラインからもカスタム関数を使えるようになりました。

注意
----

（今の所）カスタム関数内から、Sassとして設定されている変数等は参照できないようです。本当は

```scss
$image-url-base-dir: "./assets/images";

h1.site-name {
  background: image-url("/assets/images/logo.svg") no-repeat;
}
```

などと柔軟に設定をしたいところなのですが（まあ多くの場合gulpで使うでしょうから、設定はできるでしょう）。

[node-sass][]ページにも書いている通りまだ実験段階の機能なのでご注意を。この記事は3.2.0の段階で書いています。