sass でネストの深いリストを良い感じにスタイリングする。

先に概要

  • sass では @function が書けるが、再帰で呼び出すと比較的早い段階で SystemStackError - stack level too deep と怒られる。

  • プログラミング言語だと思って見ると sass での変数の扱いと配列の扱いが難しい。

  • redcarpet の問題なのか、リストを 6 段階以上ネストさせると出力が消える。正しい仕様と挙動が分からないので、ちゃんと検証したら issue を立てたい。

詳細

Markdown では以下の様に書くと

- list1
- list2

次の様に出力される。

  • list1
  • list2

簡単にリストが書けて大変便利でよく使う訳だけど、これはネストさせることが出来る。

- list1
    - list2
- list3
    - list4
        - list5
    - list6
        - list7
            - list8
                - list9

するとこんな感じになる。

  • list1
    • list2
  • list3
    • list4
      • list5
    • list6
      • list7
        • list8
          • list9

いくらでも階層を深くさせられるので大変便利。

ここに良い感じな css をあてたいと考えてしまった。

具体的には

  • 1 段階目は
  • 2 段階目は
  • 3 段階目は
  • 以下ループ

リストには順序付きのものもある。

  • 1 段階目は 1.
  • 2 段階目は i.
  • 3 段階目は a.
  • 以下ループ

となるように list-style-type を調整したい。

要するに <ul><ol> 要素に対して、それぞれの親要素の <ul><ol> を数え上げて 3 で剰余を求めて 0, 1, 2 で場合分けして style を指定出来れば良い。

そんな便利な指定方法はない。(ないよね?)

いっちょやったろうと思って sass@function などを駆使して実装してみた。

実装

sass でプログラミングをするのは初めてなので、とりあえず適当な Int を放り込むと剰余から該当する <ul> 用の list-style-type が返ってくる君を実装してみた。

// $i % 3 == 1 -> disc
// $i % 3 == 2 -> circle
// $i % 3 == 0 -> square
@function ul_style_for($i) {
    $ul_styles: (square, disc, circle); // scss の Index は 1 始まりなので、配列の方をずらすと上手く行く。
    $index: ($i % length($ul_styles) + 1);
    @return nth($ul_styles, $index);
}

この段階で割と後悔が強い。まず配列が難しい。

$list: "1.1" "1.2" "1.3", "2.1" "2.2" "2.3", "3.1" "3.2" "3.3";

こう書くと二次元配列として扱えるらしい。なるほど。

というか sass の世界では配列と変数の区別があるのか疑わしい。見た感じ無さそうだなと思う。配列というか単にデリミタとして空白とカンマを使っているだけのようにも見える。調べてはいない。

そして for のループカウンタや配列の index は 1 から始まる。確かに css を書くような局面では 1 から始まった方が都合が良い場合が多いのかもしれない。

数値の扱いも難しい。よく考えたら sass では 100px * 2 などが実現されているのだから難しいに決まっているのであった。

しれっと出てきたが、配列から要素を取り出すには nth() を使うことになっている。

めげずに <ol> 用のものも実装する。

// $i % 3 == 1 -> decimal
// $i % 3 == 2 -> lower-roman
// $i % 3 == 0 -> lower-alpha
@function ol_style_for($i) {
    $ol_styles: (lower-alpha, decimal, lower-roman);
    $index: ($i % length($ol_styles) + 1);
    @return nth($ol_styles, $index);
}

とりあえず上記で動いてることは地味な print デバッグなどを駆使してなんとか確認出来た。なぜこんなことを始めてしまったのか。

損切りのタイミングをミスったため最後までやっていく。

最終的には愚直に書かれた css をコンパイルして出力することになるため、無限を無限のまま扱うことは出来ない。無限のネストに対する指定は諦め、現実的な深さのネストに対する style の指定をすることになる。

そうするとネスト数を指定できるように書いた方が都合が良いので下記の様になった。

// $i: 1 -> ""
// $i: 2 -> ul, ol
// $i: 3 -> ul ul, ul ol, ol ul, ol ol
// $i: 4 -> ul ul ul, ul ol ul, ul ol ul, ul ol ol, ol ul ul, ol ol ul, ol ol ul, ol ol ol
// $i: 5 -> (略)
$list_tags: (ul, ol);
@function xl_prefixes_for($i) {
    @if ($i == 1) {
        @return ""; // ここは空配列じゃなく空文字を返す。
    }
    @else if ($i == 2) {
        @return $list_tags;
    }
    @else {
        $p: (); // ここは空文字ではなく空配列にする。
        @each $prefix in xl_prefixes_for($i - 1) {
            @each $list_tag in $list_tags {
                $p: append($p, #{$list_tag $prefix});
            }
        }
        @return $p;
    }
}

これを実際に使って 6 段目までのネストに対応する css を出力するには以下の様に書けばいい。

@for $i from 1 through 6 {
    @each $prefix in xl_prefixes_for($i) {
        #{$prefix} ul {
            list-style-type: ul_style_for($i);
        }
        #{$prefix} ol {
            list-style-type: ol_style_for($i);
        }
    }
}

@function xl_prefixes_for($i) で再帰が使われている。

現状は 6 にしてあるが 9 くらいまでは行ける。自分の環境ではこのコードで 10 にすると sass コンパイル時に SystemStackError - stack level too deep の例外を吐いて死ぬ。

このコードは O(2^n) な訳で、そらそうだなという気持ちになった。

どうせ nginx 側で gzip されるし、圧縮率の良さそうな出力だなぁ…という感想です。

締め

一応このページの css に使われているものの、こんなことに頑張ってもしょうがなかったなという気持ちになった。

なおデバッグ中に Markdown のネスト表記をガンガン深くしていったところ、6 段目あたりで redcarpet からの出力が消えることが確認されたのだが、環境依存なのか、仕様なのかは調べていないので分からない。余力があれば調べてなんとかしたい。