(2014年2月15日追記)よりよい解決方法として「CSSプリプロセッサやポストプロセッサで出力されたCSSの整形」「Grunt.jsでCSSの整形を行うgrunt-cssprettyを公開」を書きましたのでご覧下さい。
Less & Sass Advent calendar 2011の17日目は、@_hideki_aがSassの出力ソースをカスタマイズする方法について書かせていただきます。
3日目の「Sass を今すぐ実務で使おうよ!」ほか様々な記事で紹介されている通り、SassはCSSの設計を強力にサポートしてくれる素晴らしい言語です。しかしながら、学習コストや環境設定など多少のハードルがあり制作チーム全員がすぐに導入するのは難しいという場合や、納品後の運用はクライアントさんが行うためにSassの導入はしてもらえないという場合もあるのではないでしょうか。
そのようなことから、構築時はSassでCSSを設計し、運用ではSassで生成したCSSファイルを直接編集するという場面も想定されます。その際、「従来から決められている社内のCSS記述ルールに沿ったCSSを生成しなければならない」という問題に遭遇する可能性が考えられます。
CSS記述ルールの例
私が以前所属していた会社では、セレクタの後や値の直前にスペースを入れたり、インデントを入れたりせず、次のように記述するのが一般的なルールでした。
#main{
color:#fff;
background-color:#000;
}
#main p{
width:10em;
}
.foo{
font-size:10px;
font-weight:bold;
text-decoration:underline;
}
他の会社のルールを見ると、{と値の前にスペース1つを入れ、プロパティの前はタブ1つでインデントをすると規定されていました。
#main {
color: #fff;
background-color: #000;
}
#main p {
width: 10em;
}
.foo {
font-size: 10px;
font-weight: bold;
text-decoration: underline;
}
その他、インデントはスペース4つにするなど会社ごとに様々なルールが見られました。Sassで出力するCSSを自社のCSS記述ルールに合わせた出力にする簡単な方法はないでしょうか?
Sassの「expanded」スタイルのカスタマイズで解決
先に紹介した記述ルールはどれもSassの出力形式の1つ、「expanded」スタイルに似ています。(セレクタの後ろにスペース1つ、プロパティの前に2スペース。ネストした記述が終わるごとに1つの空行。)
#main {
color: #fff;
background-color: #000;
}
#main p {
width: 10em;
}
.foo {
font-size: 10px;
font-weight: bold;
text-decoration: underline;
}
そこで、この「expanded」スタイルの出力をカスタマイズし、自社の記述ルールに合わせたCSSを出力できるようにする方法をご紹介します。
カスタマイズ準備
カスタマイズするのは、Sassを構成するClassの中の「Class: Sass::Tree::Visitors::ToCss」になります。実際のファイルは次の場所に格納されています。念のためバックアップを取ってからカスタマイズに入りましょう。
なお、Sassのバージョンによりファイルの内容が若干異なるようです。本記事では、Sass Ver.3.1.11 / 3.1.12をベースに記述してあります。その他のバージョンをご利用の方は、記載内容をよくご確認の上マージして下さい(最新バージョンでのカスタマイズ可否もご覧下さい)。
- Windows
- C:\Ruby\lib\ruby\gems\(RubyGemsバージョン)\gems\sass-3.1.12\lib\sass\tree\visitors\to_css.rb
- Mac
- /Library/Ruby/Gems/(RubyGemsバージョン)/gems/sass-3.1.12/lib/sass/tree/visitors/to_css.rb
- Macの場合は、ファイルの所有権を自分(ログインしているユーザー)に変更しておいてください。
(2012年1月21日追記)Scoutを利用している方
Scoutの中にはコマンドラインでインストールするSassが同梱されており、Scoutを使わない場合と同じ手法でカスタマイズ可能です。カスタマイズ対象ファイルは下記となります(Ver.0.5.0の場合)。
- Windows
- C:\Program Files\Scout\vendor\gems\gems\sass-3.1.7\lib\sass\tree\visitors\to_css.rb
- Mac
- /Applications/Scout.app/Contents/Resources/vendor/gems/gems/sass-3.1.7/lib/sass/tree/visitors/to_css.rb
カスタマイズ例
カスタマイズするにあたり、expanded以外のスタイルに影響を及ぼさない手法を使うようにしました。to_css.rbファイルはRubyで記述されていますが、コピー&ペーストでカスタマイズできるようにまとめましたので、気軽にトライしてみてください。
- ちなみに私もRubyは初めてです。
- 行数はデフォルトの状態の行数を示しています。Scoutの場合は行数が若干異なりますが、同じ記述が見つかると思います。
}の後に必ず改行を入れる
178行目の後に下記を追加します。
if node.style == :compact
properties = with_tabs(0) {node.children.map {|a| visit(a)}.join(' ')}
to_return << "#{total_rule} { #{properties} }#{"\n" if node.group_end}"
elsif node.style == :compressed
properties = with_tabs(0) {node.children.map {|a| visit(a)}.join(';')}
to_return << "#{total_rule}{#{properties}}"
elsif node.style == :expanded
properties = with_tabs(@tabs + 1) {node.children.map {|a| visit(a)}.join("\n")}
to_return << "#{total_rule} {\n#{properties}\n}\n"
else
properties = with_tabs(@tabs + 1) {node.children.map {|a| visit(a)}.join("\n")}
end_props = (node.style == :expanded ? "\n" + old_spaces : ' ')
to_return << "#{total_rule} {\n#{properties}#{end_props}}#{"\n" if node.group_end}"
end
{の前の空白を削除する
178行目の後に下記を追加します。
if node.style == :compact
properties = with_tabs(0) {node.children.map {|a| visit(a)}.join(' ')}
to_return << "#{total_rule} { #{properties} }#{"\n" if node.group_end}"
elsif node.style == :compressed
properties = with_tabs(0) {node.children.map {|a| visit(a)}.join(';')}
to_return << "#{total_rule}{#{properties}}"
elsif node.style == :expanded
properties = with_tabs(@tabs + 1) {node.children.map {|a| visit(a)}.join("\n")}
to_return << "#{total_rule}{\n#{properties}\n}#{"\n" if node.group_end}"
else
properties = with_tabs(@tabs + 1) {node.children.map {|a| visit(a)}.join("\n")}
end_props = (node.style == :expanded ? "\n" + old_spaces : ' ')
to_return << "#{total_rule} {\n#{properties}#{end_props}}#{"\n" if node.group_end}"
end
セレクタが1行にまとめて列挙されるのをセレクタごとに改行する
123行目を下記に置き換えます。
def visit_rule(node)
with_tabs(@tabs + node.tabs) do
rule_separator = node.style == :compressed ? ',' : node.style == :expanded ? ",\n" : ', '
プロパティの前の空白を削除する
115行目の後に下記を追加します。
if node.style == :compressed
"#{tab_str}#{node.resolved_name}:#{node.resolved_value}"
elsif node.style == :expanded
"#{node.resolved_name}: #{node.resolved_value};"
else
"#{tab_str}#{node.resolved_name}: #{node.resolved_value};"
end
また、60行目を下記に置き換えます。
def visit_comment(node)
return if node.invisible?
if node.style == :expanded
spaces = ""
else
spaces = (' ' * [@tabs - node.resolved_value[/^ */].size, 0].max)
end
- Scoutの場合は
node.resolved_value
がnode.value
となります。
プロパティの前の空白をタブに変更する
115行目の後に下記を追加します。
if node.style == :compressed
"#{tab_str}#{node.resolved_name}:#{node.resolved_value}"
elsif node.style == :expanded
"\t#{node.resolved_name}: #{node.resolved_value};"
else
"#{tab_str}#{node.resolved_name}: #{node.resolved_value};"
end
また、60行目を下記に置き換えます。
def visit_comment(node)
return if node.invisible?
if node.style == :expanded
spaces = ("\t" * [@tabs - node.resolved_value[/^ */].size, 0].max)
else
spaces = (' ' * [@tabs - node.resolved_value[/^ */].size, 0].max)
end
- Scoutの場合は
node.resolved_value
がnode.value
となります。
プロパティの前の空白を4スペースにする
115行目の後に下記を追加します。
if node.style == :compressed
"#{tab_str}#{node.resolved_name}:#{node.resolved_value}"
elsif node.style == :expanded
" #{node.resolved_name}: #{node.resolved_value};"
else
"#{tab_str}#{node.resolved_name}: #{node.resolved_value};"
end
また、60行目を下記に置き換えます。
def visit_comment(node)
return if node.invisible?
if node.style == :expanded
spaces = (' ' * [@tabs - node.resolved_value[/^ */].size, 0].max)
else
spaces = (' ' * [@tabs - node.resolved_value[/^ */].size, 0].max)
end
- Scoutの場合は
node.resolved_value
がnode.value
となります。
値の前の空白を削除する
115行目の後に下記を追加します。
if node.style == :compressed
"#{tab_str}#{node.resolved_name}:#{node.resolved_value}"
elsif node.style == :expanded
"#{tab_str}#{node.resolved_name}:#{node.resolved_value};"
else
"#{tab_str}#{node.resolved_name}: #{node.resolved_value};"
end
カスタマイズする行が重複している場合の対応
例えば、プロパティの前、及び値の前の空白を削除したい場合は、2つのカスタマイズ例をマージ(融合)したものを記述します。具体的には、115行目の後に#{tab_str}
とコロンの後のスペースを削除した下記コードを追加するようになります。
if node.style == :compressed
"#{tab_str}#{node.resolved_name}:#{node.resolved_value}"
elsif node.style == :expanded
"#{node.resolved_name}:#{node.resolved_value};"
else
"#{tab_str}#{node.resolved_name}: #{node.resolved_value};"
end
補足
to_css.rb内のinitializeメソッドについて
initializeメソッドでインデント量が設定できそうですが、実際に使ってみると問題がありました。プロパティだけではなく、ファイル全体が2スペース×@tabsの値分だけインデントされてしまいます。
【既知の問題】@media内でインデントを行った場合について
例えば、def visit_directiveをカスタマイズすることにより、独自ルールで整形可能です。@media print {}
内でインデントをして記述したものをCSSに変換しても、タブ/スペースインデントが意図通り入らない問題があります。
デフォルトの仕様
プロパティ:値;
と同一行にコメントを記述しても、値の後で必ず改行されて出力されます。
まとめ
Classファイルを書き換えてしまうという少々荒い手法ですが、紹介させていただいたカスタマイズ例のいずれかを使用すればどのようなCSSの記述ルールにも対応でき、自分一人だけでもSassを導入してCSSの設計を始めることができると思います。本記事がSass導入の一助になれば幸いです。いずれはみんなでSassを利用して、効率化が図れると良いですね!
付録:カスタマイズしたto_css.rbファイルのダウンロード(Sass 3.1.11 / 3.1.12用)
サンプルとして、セレクタの後や値の直前にスペースを入れたり、インデントを入れたりせず、ルールごとに必ず改行を入れるタイプ(私が以前所属していた会社の記述ルール)のカスタマイズをしたto_css.rbファイルを公開します。カスタマイズ前のファイルと比較するなどして参考にして頂けたらと思います。
- Sass 3.1.13〜・Scoutを利用されている方は、上書きをせずマージしてください。
最新バージョンでのカスタマイズ可否
Sass Ver.3.1.13
行数が若干異なるものの、上記コードをそのままコピー&ペーストすることでカスタマイズ可能なことを確認いたしました。(2012年2月5日)
Sass Ver.3.1.15
行数が若干異なるものの、上記コードをそのままコピー&ペーストすることでカスタマイズ可能なことを確認いたしました。(2012年2月10日)
Sass Ver.3.1.19〜3.1.20
今まで同様にカスタマイズ可能でした。
なお、本ブログ記事初回公開後の研究で@media内も独自ルールで整形可能なことが分かりました。下記のようにセレクタの後や値の直前にスペースを入れたりインデントを入れたりせず、ルールごとに必ず改行を入れるタイプをサンプルとして掲載します。
// SCSS
#main {
width: 700px;
}
@media screen and (max-width: 959px) {
#main{
width: 75%;
}
}
// CSS
#main{
width:700px;
}
@media screen and (max-width: 959px){
#main{
width:75%;
}
}