🤔

LaTeXのlistで段落見出しをやろうとしてはいけない

2020/10/30に公開

LaTeXの箇条書きでは、各項目を \item ... で表現する。この \item には、\item[...] ... という記法で見出しも指定できて、この見出しのスタイルは、 \makelabel を使って制御できるようになっている。

ただし「\item[...] で実現できる見出し」には、実はあまり自明ではない制約がある。それは、基本的に「行見出し」が想定されているという点だ。ここで行見出しというのは、見出しだけで段落が形成されず、そのあとに続く本文と見出しが1つの段落として扱われるようなたぐいの「見出し」を指す。

一方、見出しだけで段落を形成するようなものは「段落見出し」と呼ばれる。一般的に「見出し」と聞いて想像するような節タイトルとかはこちら。HTMLには連想リスト(俗に「見出し付き箇条書き」などと呼ばれている)を表現する<DL> があるけど、この中の <DT> によって表されるタイトルも、一般的なブラウザのデフォルトでは段落見出しとしてレンダリングされることが多い。

「見出し付き箇条書き」というと、LaTeXには description 環境がある。 description 環境でも、項目とその見出しの指定は \item[...] だ。しかし、これは前述のように、あくまでも行見出しである。ということは、HTMLのような「段落見出しになる見出し付き箇条書き」は実現できないということになる。

とはいえ、それっぽいものならできる。

% 簡単のため \usepackage{enumitem} を使っているけど list 環境で定義し直しても同じ
\begin{description}[labelwidth=\linewidth]
  \item[title 1] description 1.
  \item[title 2] description 2.
\end{description}

しかし、これは段落見出しではないので、見出しが版面を越えて延びても改行はされない。

\begin{description}[labelwidth=\linewidth]
  \item[very very very very very long title] description 1.
  \item[title 2] description 2.
\end{description}

ここで考えられるのは、\makelabel を「段落ボックスで再定義する」という作戦だろう。これは、見た目の調整は必要だろうけど、動くことは動く。

\begin{description}[before =\def\makelabel{\sffamily\bfseries\parbox{\linewidth}}]
  \item[very very very very very long title] description 1.
  \item[title 2] description 2.
\end{description}

これはもはや十分に段落見出しとして機能しているのではないだろうか?

実は、これもやはりしょせん行見出しなのだ。もし段落見出しなら、見出しに続く本文に箇条書き環境が入ってもきちんと機能するはずだが、初見では何が起きているのかわからない結果が得られてしまう。

\begin{description}[before =\def\makelabel{\sffamily\bfseries\parbox{\linewidth}}]
  \item[very very very very very long title]
    \begin{itemize}
      \item subitem 1
      \item subitem 2
    \end{itemize}
  \item[title 2]
    description 2.
\end{description}

この現象は、タイトルの長さには関係なくて、段落見出しを description(より一般には list)で模倣しようとするとだいたい悩まされる。

\begin{description}[labelwidth=\linewidth]
  \item[title 1]
    \begin{itemize}
      \item subitem 1
      \item subitem 2
    \end{itemize}
  \item[title 2]
    description 2.
\end{description}

何が起きているのか

何が起きているかを知る(そしてこの問題を解消する)には、 \item の定義を見る必要がある。LaTeXの \item は、 \@item という下請けのマクロを使って定義されているが、この \@item の定義は下記のようになっている。

\def\@item[#1]{%
  \if@noparitem
    \@donoparitem
  \else
    \if@inlabel
      \indent \par
    \fi
    \ifhmode
      \unskip\unskip \par
    \fi
    \if@newlist
      \if@nobreak
        \@nbitem
      \else
        \addpenalty\@beginparpenalty
        \addvspace\@topsep
        \addvspace{-\parskip}%
      \fi
    \else
      \addpenalty\@itempenalty
      \addvspace\itemsep
    \fi
    \global\@inlabeltrue
  \fi
  \everypar{%
    \@minipagefalse
    \global\@newlistfalse
    \if@inlabel
      \global\@inlabelfalse
      {\setbox\z@\lastbox
       \ifvoid\z@
         \kern-\itemindent
       \fi}%
      \box\@labels
      \penalty\z@
    \fi
    \if@nobreak
      \@nobreakfalse
      \clubpenalty \@M
    \else
      \clubpenalty \@clubpenalty
      \everypar{}%
    \fi}%
  \if@noitemarg
    \@noitemargfalse
    \if@nmbrlist
      \refstepcounter\@listctr
    \fi
  \fi
  \sbox\@tempboxa{\makelabel{#1}}%
  \global\setbox\@labels\hbox{%
    \unhbox\@labels
    \hskip \itemindent
    \hskip -\labelwidth
    \hskip -\labelsep
    \ifdim \wd\@tempboxa >\labelwidth
      \box\@tempboxa
    \else
      \hbox to\labelwidth {\unhbox\@tempboxa}%
    \fi
    \hskip \labelsep}%
  \ignorespaces}

見出しに関係するところだけ大まかに要約すると、\makelabel で整えられた見出しの材料がいったん \box\@labels というボックスに格納され、このボックスがやはり \@item の中で用意されている \everypar によって段落の先頭に追加されることで、最終的に紙面上に組版されるという仕掛けだ。

ただし、いまはこの「見出しの作られ方」はあまり気にしなくてもいい。むしろポイントは、TeXが水平モードに入るタイミングにある。

原稿中の \item に遭遇したTeXは、まず \@inlabel フラグをオンにする[1]\@inlabel フラグがオンの場合には、\lastbox を捨てて(つまりインデントを帳消しにして)垂直方向のグルーを挿入したあと、\box\@labels を配置する。そのあとではじめて、\item に続いて原稿中で示されているトークンの存在によって水平モードに入る。つまりTeXは 、\item[...] で見出しが指示されていて、それがたとえ段落ボックスであっても、そこでは水平モードには入らない。この見出しを段落ボックスとして吐き出したあと、本文に入るときに、ようやく水平モードに入る。

このようなわけで、\item[...] の直後に itemize 環境を入れると、内側の itemize 環境の \item の見出しだけが水平モードに入る前に出力され、改行されずに前の行に残ってしまうという結果になる。

ワークアラウンド

解決策はそれほど難しくなくて、ようするに内側の \item の前に明示的に水平モードを開始すればいい。もちろん見た目は別に調整する必要があるけど。

\begin{description}[labelwidth=\linewidth]
  \item[title 1] \leavevmode
    \begin{itemize}
      \item subitem 1
      \item subitem 2
    \end{itemize}
  \item[title 2]
    description 2.
\end{description}

\item の定義による問題なので、こんなふうに原稿そのものを直接いじるか、あるいは description の内側にくる専用の \itemize みたいなものを定義するしかないように思える。逆にいうと、description のような list 系の環境で段落見出し付きの箇条書きをうまく定義することはできないはず(定義できたらおしえて)。

Pandocでのワークアラウンド

そもそも「段落見出しが必要な場合に行見出しのための list 系環境を使うのはどうなのか」という話でもあるんだけれど、HTMLの <DL> のような「見出し付き箇条書き」はドキュメントの要素として一般によく使われており、その見出しの表現として「段落見出し」を使いたいのも自然に思える。こうした見出し付き箇条書きをLaTeXで表現するときにいちばん直感的なのはやはり description なので、そのような変換を提供したくなるのは自然だろう。そう考えると、「LaTeXで段落見出し付きの箇条書き環境を list 系環境で定義したい」というのは、それほど無茶な要望ではないと思われる。

実際、PandocでもHTMLの連想リストに対応する構造のLaTeXでの表現は description として実装されている。しかし、それはつまり、description を再定義して「段落見出し」に対応させても、このようなMarkdownを書くと上記のような問題が発生するということでもある。

very very very very very long title
:   * subitem 1
    * subitem 2

title 2
: description 2.

じゃあどうすればいいかというと、やはり内側の箇条書きを始める前で水平リストを明示的に開始すればいい。

very very very very very long title
:   `\leavevmode`{=latex}

    * subitem 1
    * subitem 2

title 2
: description 2.
脚注
  1. 厳密にいえば \@noparitem フラグが立っているときは \@inlabel が立つことはないが、ltlists.ltx\@noparitem が立つように設定されているのは trivlist くらいっぽい? ↩︎

Discussion