Ruby で Sass るには

Ruby スクリプトで Sass を CSS に変換するにはどうするのがよいのか。
Sass の歴史にも最新動向にも詳しくはないので，間違ったことを書いてたらツッコミを入れてほしい。


# 従来

これまではどうだったのか。

## sass gem 時代

かつてその名もズバリ [sass](https://rubygems.org/gems/sass) という gem があった（今も存在している）。
これは Ruby で書かれた Sass の処理系であった。
そもそも Sass は Ruby が発祥なのだ。

今でも使うことはできる。Gemfile に

```rb
gem "sass"
```

と書いておき[^pn]，

[^pn]: pathname gem に関する警告がドバーッと出るようなら，`gem "pathname"` も書いておこう。以下同じ。

```rb
require "bundler"
Bundler.require

sass_text = <<~SASS
  .foo
    color: darken(red, 10%)
    margin-top: 10px + 4px
SASS

opts = {syntax: :sass, style: :compressed}
puts Sass::Engine.new(sass_text, **opts).render
# => .foo{color:#c00;margin-top:14px}
```

のように使える[^tilt]。実質一行で変換できる。
Sass 記法（インデント記法とも）なら `syntax: :sass` とし，SCSS 記法なら `syntax: :scss` とする。

[^tilt]: [tilt](https://rubygems.org/gems/tilt) を介して使うこともできるが本記事のテーマを外れるので割愛。

この gem には `sass` というコマンドが付属しており，このコマンドで変換することもできる。

しかし，Ruby で実装されているため変換速度は遅い。実際，大規模な Sass ファイルの変換では実行速度が問題となっていったようだ（知らんけど）。
そこで，C/C++ で実装された LibSass という高速なライブラリーが誕生した。これはあくまでライブラリーであり，他のプログラムに組み込んで使う。単独で使うものではない。

sass gem は [2019 年に終わった](https://sass-lang.com/ruby-sass)。知らんけど。

## sassc gem 時代

その LibSass を Ruby で使えるようにした gem が [sassc](https://rubygems.org/gems/sassc)。
sass gem と同じように使うことができる。
Gemfile に

```rb
gem "sassc"
```

と書いておき，

[^pn2]: この場合も必要に応じて `gem "pathname"` を入れておく。

```rb
require "bundler"
Bundler.require

sass_text = <<~SASS
  .foo
    color: darken(red, 10%)
    margin-top: 10px + 4px
SASS

opts = {syntax: :sass, style: :compressed}
puts SassC::Engine.new(sass_text, **opts).render
# => .foo{color:#c00;margin-top:14px}
```

のように書けばよい。
つまりモジュール名が Sass → SassC と変わっただけで，ほとんど sass gem と同じように使えるようになっている。

ちなみに，Rails で Sass るための [sass-rails](https://rubygems.org/gems/sass-rails) gem は，以前は sass gem を利用していたが，現在は（[sassc-rails](https://rubygems.org/gems/sassc-rails) gem を経由して）sassc gem を使うようになっている。

ここで話が終わればよかったのだが，LibSass に暗雲が垂れ込める。
C/C++ で Sass の処理系を開発し続けるのは大変であったらしく（？），開発は停滞気味になっていった。
その頃，Sass の処理系としては Dart という言語で書かれた Dart Sass があり，そちらは開発が進んでいた。
そして 2020 年 10 月，ついに LibSass は [deprecated になってしまう](https://sass-lang.com/blog/libsass-is-deprecated)。
保守は続けるが，新機能の取り込みなどは行わないことが決定。
今後は Dart Sass を使うように，とのこと。

えっ，なに，ちょっ，早過ぎね？　Ruby 版やめて LibSass 版を使えって言うから切り替えたばかりなのに。
つか，Dart ナニソレ？　Ruby でどうやって使うのさ？？？

# でどうすんの？

個人的には Dart に全く馴染みがない。
「Google が JavaScript の代替として Chrome に組み込んで他のブラウザーにも追随させようとして諦めた言語だっけ？」くらいのあやふやな認識だった。
しかし，Ruby 版が遅いから C/C++ 版を作ったのに Dart で実装したらまた遅くならないのかな（実際 LibSsass より遅いようだがよく知らない）。なんでまた Dart なんだろう。

Sass の新機能が必要なければ現状の sassc gem をずっと使い続ければいいか？とも思ったけどそうでもないようだ。
CSS 標準が進化しているのに LibSass がそれに追随できていないというのがそもそも問題であるらしい。
なんにしても，Sass のコアチーム（？）が公式に LibSass を deprecated にし，Dart Sass を推奨している以上，あえてそれを使わないのは不安だ。

## sass-embedded gem

Slack の ruby-jp で相談したところ，Dart Sass を Ruby で使うための [sass-embedded](https://rubygems.org/gems/sass-embedded) という gem があることを教えていただいた。

これが sassc gem の代替になっていくのかどうかは分からないけれど，とりあえず試しに使ってみることにした。

使い方は，sass gem や sassc gem とは違うものの，同様にシンプルで，Gemfile に

```rb
gem "sass-embedded"
```

と書いておいて，

```rb
require "bundler"
Bundler.require

sass_text = <<~SASS
  .foo
    color: darken(red, 10%)
    margin-top: 10px + 4px
SASS

params = {
  data: sass_text,
  indented_syntax: true,
}

# expanded（デフォルト）で出力する場合
puts Sass.render(**params).css
# => .foo {
#      color: #cc0000;
#      margin-top: 14px;
#    }

# compressed で出力する場合
puts Sass.render(**params, output_style: "compressed").css
# => .foo{color:#c00;margin-top:14px}
```

のような感じでいけた。

（2022-01-15 追記）この書き方は sass-embedded 0.10.0 で deprecated になった。記事末の追記参照。

`render` メソッドは CSS テキストを返すわけではなく，Sass::Embedded::RenderResult オブジェクトを返すようだ。
このオブジェクトの `css` メソッドで CSS テキストが返る。

Sass テキストを与えるときは上の例のように `data` オプションを使い，Sass ファイル（のファイルパス）を与えるときは `file` オプションを使う。
出力のスタイルは `output_style` オプションで切り替える。

`indented_syntax` オプションでインデント構文（Sass 構文）か SCSS 構文かを切り替えるらしい（？）。

### output_style

Sass の仕様によれば，`output_style` は以下の四つが使えるはず。

* `expanded`
* `compressed`
* `nested`
* `compact`

参考：https://sass-lang.com/documentation/js-api/interfaces/LegacySharedOptions#outputStyle

ところが，どういうわけか `"nested"` や `"compact"` を与えると

```
output_style must be one of :expanded, :compressed (ArgumentError)
```

と例外が発生する。

ここ↓で弾いてた。なぜ？
https://github.com/ntkme/sass-embedded-host-ruby/blob/v0.9.3/lib/sass/embedded/compile_context.rb#L211-L220

### BOM がつくことがある

sassc gem では，非 ASCII 文字を含んだ Sass で出力スタイルに `compressed` を選ぶと先頭に BOM（Byte Order Mark；U+FEFF）が付く。
参考：[Sass の吐き出す BOM に注意！ - Qiita](https://qiita.com/scivola/items/c6a2f60ebee5aab6f1b8)

これは sass-embedded gem でも同じだった。

### モジュール名は Sass

sass-embedded gem のモジュール名（大元というかルートというか大枠というかの）は，`Sass` である。`SassEmbedded` とかではない。
https://github.com/ntkme/sass-embedded-host-ruby/blob/v0.9.3/lib/sass.rb#L7

このことは，sass-embedded gem が sass gem と両立しないことを意味する。
もちろん両方いっぺんに使うことなんてふつうは無いわけだけど，たとえばベンチマークテストとかしようと思ったら問題になる。

### require

gem のなかには，gem 名と `require` すべきものの名前が一致しないものがあるが，sass-embedded gem はどうだろうか。
Bundler を使う限り自動的に `require` してくれるので意識する必要はないが，たとえば irb で動作確認したり，ちょこっとした瞬間書き捨てスクリプトなんかでは Bundler を使わずに `require` することもあるので，知っておきたい。

結論から言えば，

```rb
require "sass-embedded"
```

でも

```rb
require "sass"
```

でもどちらでもいける。
前者は単に `require_relative 'sass'` しているだけ：
https://github.com/ntkme/sass-embedded-host-ruby/blob/v0.9.3/lib/sass-embedded.rb#L4

したがって，sass gem と sass-embedded gem が両方インストールされている環境下では Bundler を使わないと意図しないほうを読んじゃう恐れがある，ということになる。要注意。

## Node.js の sass パッケージ

私は Node.js についての理解がかなり浅いのでなんか間違ったことを書くかも。ぜひツッコミを。
Node.js には sass というパッケージがあり，~~（昔は違ったようだけど）現在は~~ Dart Sass を利用するものとなっている[^node-sass]。
（このことも Slack の ruby-jp で教えていただいた）

[^node-sass]: （2022-01-20 追記）これとは別に node-sass というパッケージがあり，こちらは Dart Sass でなく LibSass に基づいている。

これを利用するには，まず Node.js をインストールして[^njsi]，コマンドラインで

```
npm install -g sass
```

とすればいいようだ。
`-g` というオプションは，（プロジェクトのローカルではなく）グローバルにインストールする，という意味らしい。こうしておけばどこからでもコマンドラインで sass コマンドが使える。
このコマンド名は sass gem によってインストールされるものと競合してしまうので，注意しよう。

[^njsi]: 私でも（macOS，CentOS および Windows に）インストールできたので簡単である。

確認はコマンドラインで

```
sass --version
```

とすれば

```
1.43.4 compiled with dart2js 2.14.4
```

のように表示される。これを見れば，sass gem 版の sass コマンドではないことや，なんかよくわからんけど，Dart Sass ぽいことなどが分かる。
（どうも Dart 言語のコードを JavaScript に変換したものを JavaScript の処理系で実行しているらしいのだがよく知らん。つか JavaScript にしたら Dart よりもっと遅くならんの？）

### 使い方

ここでは，Ruby から Node.js の sass コマンドを使うことを考える。
Sass テキストを標準入力で与えて，結果を標準出力からもらえばいい。

sass コマンドに，Sass テキストを与えるには，`--stdin` オプションを付ければよいようだ。
また，インデント構文（Sass 構文）の Sass テキストを与えるなら `--indented` オプションを付ける。付けなければ SCSS 構文とみなされる。
出力スタイルは `--style compressed` のようにして与える。デフォルトは `expanded` だ。

これを用いる Ruby スクリプトは，[IO.popen](https://docs.ruby-lang.org/ja/3.0.0/method/IO/s/popen.html) を用いてこんな感じに書ける：

```rb
sass_text = <<~SASS
  .foo
    color: darken(red, 10%)
    margin-top: 10px + 4px
SASS

css = IO.popen("sass --stdin --indented --style compressed", "r+") do |io|
  io.write(sass_text)
  io.close_write
  io.read
end

puts css
# => .foo{color:#c00;margin-top:14px}
```

現実のコード（とくにライブラリーの中）などでは，sass コマンドの存在確認や，それが Node.js 版であることの確認などを行って，不足なら「どうすればよいか」のメッセージを出したりする必要があるだろう。

### 出力スタイル

どうやら Node.js 版の sass も出力スタイルは `expanded` と `compressed` しか指定できないようだ。
`compact` を与えると

```
"compact" is not an allowed value for option "style".
```

というメッセージとともにヘルプ（使い方）が表示される。
え〜，なんで？
いや，私自身は expanded と compressed しか使わないからいいんだけど，Sass の公式サイトで四つのスタイルが書かれているのに，なんだかなあ。

### BOM

こちらも，非 ASCII 文字を含む場合に compressed スタイルで出力すると先頭に BOM が付くようだ。

# まとめ

* sass gem は使うべきでなく，コマンド名や require するファイルの名前の競合の問題があるので，アンインストールしておいたほうがいい。
* sassc gem も既に deprecated。
* sass-embedded gem を使うか，Node.js の sass パッケージによる sass コマンドを使う，という代替策が考えられる。

# 追記（2022-01-15）sass-embedded 0.10.0 での書き方

[sass-embedded](https://rubygems.org/gems/sass-embedded) は，2022 年 1 月 15 日にバージョン 0.10.0 がリリースされた。

このバージョンでは，「sass-embedded gem」の節に書いたような

```rb
Sass.render(data: "h1 { color: red }")
```

といった書き方が deprecated になったようだ。ただし，使うことはできるし，画面に警告は出ない。[コードのコメントに @deprecated と書いてある](https://github.com/ntkme/sass-embedded-host-ruby/blob/main/lib/sass/embedded/render.rb#L27) だけのようだ。

`render` の代わりに導入されたのが `Sass.compile` と `Sass.compile_string` で，前者はファイルパスを渡し，後者は Sass テキストを渡すものらしい。

また，Sass テキストの文法は `indented_syntax` オプションに真／偽を渡すのではなく，`syntax` オプションに `:indented`，`:scss`，`:css` のいずれかを渡すようだ。

出力スタイルは `output_style` オプションではなく `style` オプションに変わった（値は `:expanded` か `:compressed`）。

したがって，サンプルコードとしてはこうなる：

```rb
require "bundler"
Bundler.require

sass_text = <<~SASS
  .foo
    color: darken(red, 10%)
    margin-top: 10px + 4px
SASS

# expanded（デフォルト）で出力する場合
puts Sass.compile_string(sass_text, syntax: :indented).css
# => .foo {
#      color: #cc0000;
#      margin-top: 14px;
#    }

# compressed で出力する場合
puts Sass.compile_string(sass_text, syntax: :indented, style: :compressed).css
# => .foo{color:#c00;margin-top:14px}
```