satysfi-enumitem でアレな箇条書きをいっぱい作る話

この記事は [SATySFi Advent Calendar](https://adventar.org/calendars/3929) 4 日目の記事です．前日は amutake さんの「[satysfi-docker を使って SATySFi をお手軽に試す](http://amutake.hatenablog.com/entry/2019/12/03/082528)」という記事でした．

monaqa です．本記事では，一ヶ月ほど前に私が作成して公開した [enumitem](https://github.com/monaqa/satysfi-enumitem) パッケージを紹介します．

---

（2020/06/13 更新）
**※注意※** このドキュメントは v1.0.0 時点のものです．現在は satysfi-enumitem パッケージの v2.0.0 がリリースされており，大量の破壊的変更が加えられたため，**以下の記事に書かれているコードを用いても動きません**． v2.0.0 におけるパッケージの説明は [リポジトリの README](https://github.com/monaqa/satysfi-enumitem/blob/master/README-ja.md) 及び [公式ドキュメント (PDF)](https://github.com/monaqa/satysfi-enumitem/blob/master/doc/enumitem.pdf) をご確認ください．

ちなみに， v1.0.0 から v2.0.0 にかけて，以下のような機能が追加されました．

* [Satyrographos](https://github.com/na4zagin3/satyrographos) を用いて簡単にインストールできるようになった（[アドベントカレンダー7日目の記事](https://qiita.com/na4zagin3/items/14fe2647b663eeac6ac2) もぜひご覧ください）
* `+description` で定義リストが作成できるようになった
* ラベルだけでなく本文の体裁も変更できるようになった
* インデント量などをパラメータでいじれるようになった
* 本文中にフラグを指定することで，ラベルの体裁を一時的に変更できるようになった
  （たとえば，To-do リストに用いられるチェックボックスが楽に作れるようになった）

このように沢山の機能が追加されているため，基本的には v2.0.0 を使うことをお勧めします．

---

# Enumitem パッケージとは

Enumitem パッケージは，組版処理システム [SATySFi](https://github.com/gfngfn/SATySFi) において箇条書きを実現するためのパッケージです．「あれ，箇条書きって標準で用意されてなかったっけ」と思われたかもしれません．仰るとおり，SATySFi には標準で itemize というパッケージが用意されています．Enumitem パッケージは標準の itemize パッケージを拡張したものであり，具体的には以下のような特徴を備えています：

* デフォルトで豊富なスタイルを選択できる
* 番号付き箇条書き環境をネストさせることができる
* ネストごとに箇条書きのスタイルを変更できる
* ユーザ自身がスタイルを容易に拡張できる

なお，enumitem という名前は LaTeX の [enumitem パッケージ](https://www.ctan.org/pkg/enumitem) から~~パクり~~拝借しました．

# Enumitem パッケージの基本的な使い方

組版処理システム [SATySFi](https://github.com/gfngfn/SATySFi) については，この記事をご覧くださっている方は既にご存知ではないかと思います．知らないよ，という方は，是非 [SATySFi の日本語版 README](https://github.com/gfngfn/SATySFi/blob/master/README-ja.md) を読まれることをおすすめします．

## パッケージのインポート

Enumitem パッケージを使うにはインポートが必要です．[enumitem.satyh](https://github.com/monaqa/satysfi-enumitem/blob/master/enumitem.satyh) を SATySFi のライブラリルート（デフォルトでは `~/.satysfi`）に配置し，作成したい文書の冒頭に

```
@require: enumitem
```

と書けば OK です．もしくは，同ファイル ([enumitem.satyh](https://github.com/monaqa/satysfi-enumitem/blob/master/enumitem.satyh)) を作成したい文書と同一のディレクトリに配置し，作成したい文書の冒頭に以下の文を追加することでもインポートできます．

```
@import: enumitem
```

上のどちらかを行えば enumitem パッケージ特有のコマンドが使えるようになります．

## 番号なし箇条書きコマンド：+listing

まずは， SATySFi 標準の itemize パッケージにも用意されており，最も馴染み深いであろう `+listing` を紹介します．本文の適切な位置（ブロックコマンドを置くことができる場所）に，以下のように書いてみましょう．

```
+p{
  世の中には3種類の人間がいる．
}
+listing{
  * 数を数えられる人間
  * 数を数えられない人間
}
```

すると次の画像のような結果が得られます：

![are1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231599/0638f573-7a42-df16-fe58-cab6070f8e81.png)

正しく箇条書きされていることが分かります．

## 自由度の高いコマンド：+genlisting

これでは標準のパッケージとあまり変わらないので，もう少しオリジナリティのある箇条書きを書いてみましょう．`+genlisting` コマンドを使うと，ラベルのスタイルを変えることができます．前で確認したとおりデフォルトの `+listing` では黒丸のラベルが用いられますが，黒い丸ではなくたとえば白い丸を使って箇条書きしたくなったとします．そのような場合は，`+listing` の代わりに以下のように書いてみてください．

```
+genlisting(label-white-bullet){
  * あれ？
  * ちゃんと白丸になってる！
}
```

![are2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231599/a1cd56ee-1901-a593-6aa7-067818e4577d.png)

ちゃんと白丸になってますね．

`+genlisting` とは，より一般化 (generalize) された `+listing` コマンドです．LaTeX の `\genfrac` コマンドにちなんで名付けました．`+genlisting` の第1引数は〈仮面〉のように自由に付け替えることができ，対象の箇条書きの外観を変化させる効果を有します．つまり第1引数を変えることで，簡単に様々なスタイルでの箇条書きを実現することができます．たとえば，ローマ数字で上から順に (i), (ii), (iii), (iv), ... と番号を振りたくなった場合，以下のように書けば実現することができます．

```
+genlisting(label-roman-paren){
  * あれ？
  * ちゃんとローマ数字になってる！
}
```
![are3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231599/a6f45607-68df-7a28-2921-e151c6cfe69b.png)

どのような引数を取りうるかについては，[enumitem パッケージのドキュメント](https://github.com/monaqa/satysfi-enumitem/blob/master/doc/enumitem.pdf)をご覧ください．
なお，enumitem パッケージに標準で用意されている `+enumerate` コマンドでは，箇条書きのネストの深さに応じてラベルの種類が変わるようになっています[^1]．以下は，`+enumerate` コマンドを用いて [SATySFi Wiki](https://github.com/gfngfn/SATySFi/wiki) の目次の一部分を羅列したものです．

![are4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231599/b786dfad-52b4-b6f0-fd6f-256d43dbe9b7.png)

[^1]: 実は `+enumerate` のようにネストで挙動を変えるためには `+genlisting` ではなく `+genlistings` というコマンドを用いる必要があるのですが，その説明は [enumitem パッケージのドキュメント](https://github.com/monaqa/satysfi-enumitem/blob/master/doc/enumitem.pdf) に譲ります．

また，SATySFi ではインライン/ブロックコマンドをユーザ定義することができます．その機能を使えば，お気に入りの箇条書き環境をもっと短いコマンドで書くことができます．たとえば白丸の箇条書きが気に入ったなら，プリアンブル [^2] 部分で以下のようにもっと短いコマンドを定義するのが良いでしょう：

[^2]: プリアンブルとは，パッケージをインポートしてからドキュメントの記述部分が開始するよりも前の部分のことです． stdjabook パッケージを使う場合は ``document`` と書かれている箇所よりも前の部分ということになります．

```
let-block +listing-white item = '<
  +genlisting(label-white-bullet)(item);
>
```

これで，その文書では `+listing` の代わりに `+listing-white` を使うだけで，白丸の箇条書きを使えるようになります．

# 好きなスタイルの箇条書きをユーザ定義する

ラベルの外観を変化させる〈仮面〉は予め enumerate パッケージ中に22枚用意されています（2019年12月4日時点．詳しくは [ドキュメント](https://github.com/monaqa/satysfi-enumitem/blob/master/doc/enumitem.pdf) 参照）．しかし，標準のものだけでは不満だ，もっとインスタ映えするラベルを自分で作りたい，ということもあるでしょう．
Enumitem パッケージを使えば簡単に作れます．
というのも，〈仮面〉の正体はただの `ctx -> int -> inline-boxes` 型の関数だからです． 文書のテキスト処理文脈 (`ctx`) とインデックス値 [^3] (`idx`) を入力としてラベルを返す `ctx -> int -> inline-boxes` 型の関数をユーザ定義して `+genlisting` の引数に入れれば，好きなスタイルの箇条書きをそのユーザはいつでも使えるようになる，というわけです．

[^3]: そのアイテムが箇条書きの何番目にあるか．

## 最もシンプルなユーザ定義の例

SATySFi といえば，大学生がレポート課題を書くときに使用するツールとして大人気であることが知られています [^4] が，標準の itemize パッケージでは「問題1」「問題2」などのラベルが付いた箇条書きを書くことができず，自身で一からコマンドを作り直す必要がありました．
そこで，enumitem パッケージを用いてユーザ定義してみましょう．まずプリアンブルにて， `label-toi` という `ctx -> int -> inline-boxes` 型の関数を以下のように定義します．

[^4]: このアドベントカレンダーが終わる頃には LaTeX と SATySFi のシェア比率が完全に逆転する，といわれています．私の中で．

```
let label-toi ctx idx =
  let it-num = embed-string (arabic idx) in
  let ib-label = read-inline ctx {\textbf{問題#it-num;.}\ } in
  ib-label
```

あとは，本文中で以下のように `+genlisting` コマンドを使うだけです．

```
+genlisting(label-toi){
  * 象を冷蔵庫に入れるにはどうすれば良い？
  * では、キリンを冷蔵庫に入れるには？
  * 百獣の王ライオンが会議を招集した．欠席したのは？
  * 大変！二人の探検家がワニの住む沼を渡ろうとしているわ！
}
```

![are5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231599/8158c6ef-59f4-78df-9b6b-7a2ecf9cef2e.png)

`+listing` コマンドを一から実装しなおすよりも，楽に実現できました．

さて， `label-toi` 関数の定義は，今までとは違い SATySFi のプログラミングの側面がよく出たコードになっています．SATySFi の構文を読み慣れている方にとっては説明するほどでもないかもしれませんが，SATySFi でのプログラミングがはじめての方向けに少し補足します．

* 上記プログラムには **inline-text** 型と **inline-boxes** 型が登場します．両者の区別がややこしいのですが，私は inline-text の方は「出力させたいテキストの情報」，inline-boxes の方は「inline-text にテキスト処理文脈の情報を付け加えたもの」と理解しています．テキスト処理文脈 (ctx) というのは，書体，フォントサイズ，行間など，文字を「組む」ときに必要な情報です．

* 頭に `it-` と付いている変数は，基本的に inline-text 型です．同様に `ib-` と付いている変数は，基本的に inline-boxes 型です．どちらも付けないといけない決まりはありませんが，私は混乱を防ぐためによくそう命名します．

以下の関数やコマンドの知識もあるといいでしょう．

* `arabic`: `int -> string` 型．整数を受け取り，それをアラビア数字の文字列に変換する関数です．
* `embed-string`: `string -> inline-text` 型．文字列を inline-text に変換します．
* `read-inline`: `ctx -> inline-text -> inline-boxes` 型．与えられた inline-text を，与えられたテキスト処理文脈を使って「組む」ことにより，inline-boxes を作成します．
* `\textbf` というのは，中の inline-text を太字で組むよう指示するためのインラインコマンドです．LaTeX を使ったことがある人ならば違和感なく使えると思います．

他の箇条書きの例も試してみましょう．Enumitem パッケージには `Enumitem.to-roman: int -> inline-text` などの関数が予め用意されており，これを使うと整数値をローマ数字の形式で表示することができます．たとえば以下のようなラベルを定義してみます．

```
let label-roman-bold ctx idx =
  let it-num = Enumitem.to-Roman(idx) in
  let ib-label = read-inline ctx {\textbf{#it-num;:}\ } in
  ib-label
```

すると以下のような結果になります．

![are6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231599/4f1bb266-47ea-5287-f45a-2a08b5f4c434.png)

ローマ数字では I が II になるだけで数字の幅が大きく変わるため，この実装ではラベルの後のテキストの開始位置がずれてしまい，あまり箇条書きらしくありません．そこで，少し工夫してみましょう．

```
let label-roman-bold-flushright ctx idx =
  let label-width = (get-font-size ctx) *' 2.5 in
  let it-num = Enumitem.to-Roman(idx) in
  let ib-label = read-inline ctx {\textbf{#it-num;:}\ } in
  let (wid-label, _, _) = get-natural-metrics ib-label in
  inline-skip (label-width -' wid-label) ++ ib-label
```

先程よりコードが若干複雑になりましたが，やったことは右側に「行間調整用のスペース」をつけただけです（前のコードと比較してみてください）．すると以下のような出力が得られます．

![are7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231599/8874580d-3891-7b3e-f74c-ba848b709811.png)

ラベルを右揃えにし，本文の開始位置を揃えることができました．標準で用意されている `label-roman-dot` などのラベルも，実は右揃えになっています．

## もっと自由な箇条書きを作成する

上の例で示した箇条書きはいずれもラベルが 1, 2, 3, ... とインクリメントされる，いわゆる enumerate 型の箇条書きでしたが，ラベルが変わらない itemize 型の箇条書きも作成できます．以下の例を見てください．

```
let label-8 ctx idx =
  let ib-label = read-inline ctx {☃} in
   ib-label ++ (inline-skip 10pt)
```

![are8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231599/e95ad6ab-eacd-e8f9-e4a3-21d25b4eeb69.png)

楽しい箇条書きができました[^5]．
関数 `label-8` は `ctx` および `idx` を引数にとるものの，中身で `idx` を一切使いません．このようにインデックス値（そのアイテムが箇条書きの何番目にあるか）に関する定数関数を用いることで，ラベルが変化しない箇条書きをも定義することができます．

[^5]: 余談ですが，上の例でちゃんと☃を出すためには．SATySFi の欧文フォント設定時に「☃」が入ったフォントを選択する必要があります．和文フォントを用いて出力させたければ， zr_tex8r さんによる [こちらの記事](https://qiita.com/zr_tex8r/items/56dee4364b05974d7a54) を参照されると良いと思います．

また，テキスト処理文脈をインデックスに応じて変更することもできます．インデックス値を3で割った余りに応じてラベルの色を変えてみましょう．

```
let label-8-xmas ctx idx =
  let xmas-color = match idx mod 3 with
    | 0 -> Color.rgb 0.1 0.5 0.2
    | 1 -> Color.rgb 0.7 0.2 0.2
    | _ -> Color.black
  in
  let ctx-xmas = ctx |> set-text-color xmas-color in
  let ib-label = read-inline ctx-xmas {☃} in
   ib-label ++ (inline-skip 10pt)
```

![are9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231599/4cefbdc8-78bc-bacf-d79a-c85e7318cba6.png)

いい感じにクリスマスっぽい配色になりましたね！

## もっともっと自由な箇条書きを作成する

グラフィックスを利用すれば，もっともっと自由な箇条書きを作ることもできます．たとえば， ほとんど普通の `+listing` コマンドと変わらないものの「インデックス値が1増えるにつれてラベルの直径が2倍になる」という条件をつけた箇条書きを作ってみましょう．こちらも簡単に作れます．

```
let-rec power-float-int r m =
  match m with
  | 0 -> 1.
  | _ -> (power-float-int r (m - 1)) *. r

let label-baibain ctx idx =
  let circ ctx (x, y) =
    let cx = x +' 14pt in
    let cy = y +' 3pt in
    let r = 0.75pt *' (power-float-int 2.0 idx) in
    Gr.circle (cx, cy) r
  in
  inline-graphics 20pt 8pt 0pt
    (fun (x, y) -> [fill Color.black (circ ctx (x, y))])
```

![are10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231599/56ba707d-252a-c3b4-3827-45b7e9bf7d58.png)

想像以上に大変なことになりましたね．もしラベルがあと30個も続いていたら，日本列島はこの箇条書きのラベルの下に完全に埋もれてしまっていたでしょう．Enumitem パッケージを使えば，そのような危険きわまりない箇条書きすら簡単に生み出すことができてしまいます．


# おわりに

長くなってしまいましたが，最後まで読んでくださりありがとうございました．本記事で説明を省略した箇所については，[enumitem パッケージのドキュメント](https://github.com/monaqa/satysfi-enumitem/blob/master/doc/enumitem.pdf) をご覧ください．

明日の SATySFi アドベントカレンダーの記事は zr_tex8r さんの記事となる予定です．そちらも是非ご覧になってください！

Happy SATySFiing!