嵌套列表

把列表嵌套起来,LaTeX 会按层级切换每行开头的标签。可是放进 itemize 条目里的 enumerate,版面上明明是第二层,却从「1.」而不是「(a)」开始。因为 LaTeX 数的不是纸面上的深度,而是有多少个同类列表把它包在里面。不知道这一点就去写嵌套列表,看到输出时只会一头雾水。本页要讲的是:标签如何被选中、名为 Too deeply nested 的那堵墙为什么立在第四层、所有列表环境共用的另一道上限,以及每加深一层就确实失去的那部分行宽。

决定标签的是同类嵌套的层数,不是版面上的深度

LaTeX 用两个各自独立的计数器来数深度。在 latex.ltx 中,itemize 推进 \@itemdepthenumerate 推进 \@enumdepth;两者各自用自己的值拼出 \labelitem\labelenum 加罗马数字的命令名再调用。二者互不通气,所以 itemize 里的 enumerate\@enumdepth 上就是第 1 层——于是用 \labelenumi,编号从「1.」开始。常有人说「标签一律由嵌套深度决定,与类型无关」,这是错的。实际排一次就一目了然。

latex
\begin{enumerate}
  \item Make the dough
  \begin{itemize}
    \item Flour
    \item Water
    \begin{enumerate}
      \item Add half
      \item Add the rest
    \end{enumerate}
  \end{itemize}
  \item Let it rest
\end{enumerate}

这三层嵌套实际排出来是这样:外层 enumerate 给出「1.」「2.」;它里面的 itemize 拿到的是第一层的圆点 •,而不是第二层的破折号 –;最里层的 enumerate 拿到的是第二层的 (a),而不是第三层的罗马数字 i.。因为在 itemize 看来这是第一个 itemize,而在内层 enumerate 看来外面只有一个 enumerate。交替地一层层套下去,每一种类型都会各自按顺序用掉自己那套标签。

深度环境该类型内的层数得到的标签
1itemize1• (\labelitemi
2enumerate11. (\labelenumi
3itemize2– (\labelitemii
4enumerate2(a) (\labelenumii
5itemize3* (\labelitemiii
6enumerate3i. (\labelenumiii

第四层的那堵墙:! LaTeX Error: Too deeply nested.

把同一类型的列表套到第五层,编译就会停在 ! LaTeX Error: Too deeply nested.。这条消息不会告诉你深度是多少——只有这三个词,究竟哪个 \begin 是第五层,得自己去数;报出的行号就是开启第五层的那行 \begin{itemize}。原因并非排版上的禁令,而只是名字不够用:latex.ltx 里的判断不过是 \ifnum \@itemdepth >\thr@@,即「是否大于 3」。标签命令只定义到第四层,第五层拿不到名字,于是就停。这也意味着光加载 enumitem 并不能拆掉这堵墙:不加任何配置就打开第五层,同样的报错会以同样的方式出现。

latex
% raising the ceiling really does take all four lines
\usepackage{enumitem}
\setlistdepth{9}
\renewlist{itemize}{itemize}{9}
\setlist[itemize]{label=$\cdot$}
\setlist[itemize,1]{label=$\bullet$}

真要拆掉这堵墙,光靠 enumitem\setlistdepth 是不够的\setlistdepth{9} 只是抬高了列表总体的上限,itemize 本身仍旧停在第四层。必须用 \renewlist{itemize}{itemize}{9} 按九层重新造出这个环境,再用 \setlist[itemize]{label=…} 给新增的层配上标签,第五层往下才排得出来。不给标签就打开第五层,报错只会换成 ! Package enumitem Error: Undefined label.。如果真正想要的是带编号的深层结构,那么像 easylist 这样从一开始就为任意深度设计的宏包更为直白。不过在动手之前请先停一停:几乎没有读者能跟得住五层嵌套。嵌套到这个深度,通常意味着这段结构应该改写成章节标题。

另一道上限:所有列表环境共享的六层预算

在「每种四层」之上,还压着一道所有列表共享的六层上限itemizeenumerate 内部都建立在同一个 list 环境之上,而 list 自带一条判断:\ifnum \@listdepth >5。所以把 itemizeenumerate 交替叠到第六层还能通过,第七层照样会得到 Too deeply nested。麻烦之处在于,花掉这份预算的并不只有列表。翻一遍 article.cls 就会看到,quotequotationversedescription,乃至 thebibliography,全都建立在 \list 之上。在很深的项目符号列表里放上两层嵌套的引用块,光这两层就能把剩余预算耗光。由于罪魁看上去根本不像列表,这类故障是最难排查的一种。

缩进是累加的:到第四层,行宽已少掉四分之一

嵌套真正的代价不体现在标签上,而体现在行的宽度上。list 环境每打开一次,就把自己的 \leftmargin 累加到左侧的总和里,于是缩进一层层堆起来。在单栏的 10pt article 中,\leftmargini 是 25.0pt,\leftmarginii 是 22.0pt,\leftmarginiii 是 18.7pt,\leftmarginiv 是 17.0pt。到第四层时左边距合计 82.7pt,相对 345pt 的正文宽度已经去掉了大约四分之一,剩下可写的行宽为 262.3pt。这些数值逐层略微减小,正是为了减缓这种累加,但即便如此,四层也要削去这么多。

长度取值累计剩余行宽(textwidth 345pt)
\leftmargini25.0pt(2.5em)25.0pt320.0pt
\leftmarginii22.0pt(2.2em)47.0pt298.0pt
\leftmarginiii18.7pt(1.87em)65.7pt279.3pt
\leftmarginiv17.0pt(1.7em)82.7pt262.3pt

换成数字,深层嵌套为什么难读就变得具体了。262pt 的行宽已接近双栏排版中的一栏;把长句灌进去,断行算法的回旋余地变小,词间距会不自然地撑开,Overfull \hbox 警告也随之增多。何况在双栏排版中 \leftmargini 本来就被压到 2em,版面越窄,嵌套的余地越小。要收紧左边距,用 enumitemleftmargin= 逐层指定是实用的做法。不过多数情况下,回头调整结构比这些都快——比第三层更深的条目,通常都能作为散文并回父条目里去。

嵌套该在哪里停下

LaTeX 允许四层,但那并不等于说四层就读得下来。深层嵌套传达的正是层级结构,而读者只能凭行首符号的形状与左边缘的位置这两样东西去还原它。第三层的星号与第四层的居中点形状相近,而两者的缩进差只有 17pt。也就是说,到了第四层,读者手上几乎没剩下什么线索。下面这份清单可用于写完嵌套之后的复查。

  • 先确认两层是否已经够用。 第三层以后的内容,通常都能作为散文并回父条目。
  • 如果确实需要那样的深度,就改用章节标题而不是列表。 \subsection\subsubsection 会顺带带来目录与交叉引用。
  • 混用类型时务必看一眼输出。 标签取决于有多少个同类列表把它包住,所以拿到的符号可能与预期不同。
  • 别把 quoteverse 放进深层嵌套。 它们花的是同一份六层预算,引发的报错来源难以察觉。
  • 出现 Too deeply nested 就重新数一遍深度。 消息从不告诉你层数,只能从报出的那行往回追 \begin