4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Ruby で Sass るには

Last updated at Posted at 2021-11-20

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

従来

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

sass gem 時代

かつてその名もズバリ sass という gem があった(今も存在している)。
これは Ruby で書かれた Sass の処理系であった。
そもそも Sass は Ruby が発祥なのだ。

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

gem "sass"

と書いておき1

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}

のように使える2。実質一行で変換できる。
Sass 記法(インデント記法とも)なら syntax: :sass とし,SCSS 記法なら syntax: :scss とする。

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

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

sass gem は 2019 年に終わった。知らんけど。

sassc gem 時代

その LibSass を Ruby で使えるようにした gem が sassc
sass gem と同じように使うことができる。
Gemfile に

gem "sassc"

と書いておき,

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 gem は,以前は sass gem を利用していたが,現在は(sassc-rails gem を経由して)sassc gem を使うようになっている。

ここで話が終わればよかったのだが,LibSass に暗雲が垂れ込める。
C/C++ で Sass の処理系を開発し続けるのは大変であったらしく(?),開発は停滞気味になっていった。
その頃,Sass の処理系としては Dart という言語で書かれた Dart Sass があり,そちらは開発が進んでいた。
そして 2020 年 10 月,ついに LibSass は 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 という gem があることを教えていただいた。

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

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

gem "sass-embedded"

と書いておいて,

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

これは 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 することもあるので,知っておきたい。

結論から言えば,

require "sass-embedded"

でも

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 を利用するものとなっている3
(このことも Slack の ruby-jp で教えていただいた)

これを利用するには,まず Node.js をインストールして4,コマンドラインで

npm install -g sass

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

確認はコマンドラインで

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 を用いてこんな感じに書ける:

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 も出力スタイルは expandedcompressed しか指定できないようだ。
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 は,2022 年 1 月 15 日にバージョン 0.10.0 がリリースされた。

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

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

といった書き方が deprecated になったようだ。ただし,使うことはできるし,画面に警告は出ない。コードのコメントに @deprecated と書いてある だけのようだ。

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

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

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

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

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}
  1. pathname gem に関する警告がドバーッと出るようなら,gem "pathname" も書いておこう。以下同じ。

  2. tilt を介して使うこともできるが本記事のテーマを外れるので割愛。

  3. (2022-01-20 追記)これとは別に node-sass というパッケージがあり,こちらは Dart Sass でなく LibSass に基づいている。

  4. 私でも(macOS,CentOS および Windows に)インストールできたので簡単である。

4
4
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?