itemize

LaTeX 的 itemize 打在行首的圆点,从前一旦落进斜体文字里就会跟着倾斜——因为这个符号会原样继承周围的字体设置。2020-02-02 的发布加入了 \labelitemfont 命令,才终于止住了这个毛病。也就是说,项目符号列表里的那个点并不是装饰,而是一个和其他字符一样被排版出来的字形。本页从三个角度看 itemize:行首符号究竟定义在哪里、用 \item[...] 换掉其中一个会发生什么,以及哪两个长度决定了条目之间的间距。

圆点定义在哪里:从 \labelitemi 到 \labelitemiv

行首符号根本不在 itemize 环境里,它们写在文档类中。打开 article.cls,会看到四条并排的命令:\labelitemi\labelitemiv,依次装着 \textbullet(•)、加粗的 \textendash(–)、\textasteriskcentered(*)和 \textperiodcentered(·)。itemize 自己要做的只是数出当前处在第几层,拼出 \labelitem 加罗马数字的名字,再调用那个名字所对应的内容。所以要换符号,从来不必去动环境,替换命令即可。第四层之后什么都没有,道理同样简单:从来没有定义过第五个名字。

命令层级符号article.cls 中的内容
\labelitemi1\labelitemfont \textbullet
\labelitemii2\labelitemfont \bfseries \textendash(加粗)
\labelitemiii3*\labelitemfont \textasteriskcentered
\labelitemiv4·\labelitemfont \textperiodcentered
\labelitemfont\normalfont;把符号与周围字体隔开

表格最后一行的 \labelitemfont 就是开头提到的 2020 年那次修复。四个符号都以它开头被调用,所以如今把列表放进斜体或粗体的段落里,符号本身仍会保持正立。反过来也可以利用它,让符号单独使用另一种字体:写 \renewcommand{\labelitemfont}{\normalfont\sffamily},正文仍是衬线体,而行首符号变成无衬线。要替换整整一层的符号,用 \renewcommand 覆盖对应的 \labelitem…。放在导言区则作用于全文,放在环境或分组内则只作用于该处。

latex
% swap the first-level marker for the whole document
\renewcommand{\labelitemi}{$\diamond$}

\begin{itemize}
  \item this level now uses a diamond
  \item so does this one
\end{itemize}

用 \item[...] 只替换一个条目的符号

\item 加上方括号参数,该条目的行首符号就会被括号里的内容取代。\item[$\star$] 给出星号,\item[--] 给出破折号,\item[Note] 则把「Note」这个词直接排在符号的位置上。这里有一个流传很广的误解需要纠正:itemize 里的方括号标签并不会加粗。加粗的是 description 环境,它的 \descriptionlabel 确实包含 \bfseriesitemize 的标签排版只是 \hss\llap{…}——只做右对齐,不碰字体。

latex
\begin{itemize}
  \item the default marker
  \item[$\star$] a star for this one entry
  \item[Note] a whole word in the marker slot
\end{itemize}

这个 \llap 也埋下了一个坑。标签被右对齐地放进一个宽为 \labelwidth 的盒子里,而 \llap 溢出时不会有任何提示\item[Note] 还放得下,但换成 \item[Prerequisite] 这样的长词,标签就会一路向左伸出正文区,严重时甚至跑到版面之外。行距不会乱,也不会有 Overfull \hbox 警告,所以在看到 PDF 之前根本察觉不到。若想用词语作标签,那已经是 description 的活儿,而不是 itemize 的;用 enumitem 的话,可以用 labelwidth=leftmargin= 预留出宽度。

条目之间太松时:\itemsep 与 \parsep

两个条目之间的间距不是一个长度,而是两个之和\item 在开始下一条之前插入 \itemsep;紧接着新的段落开始,于是又加进 \parskip——而在列表内部,\parskip 被设成了 \parsep。所以真正的间距是 \itemsep + \parsep。在 10pt 的 article 中,第一层两者都是 4pt,合计 8pt;相对于 12pt 的行距,条目之间就留出了三分之二行的空白。人们说列表「看着松垮」,通常看到的正是这个;也正因如此,只把 \itemsep 归零,只能收紧一半。

长度第一层默认值(10pt 的 article)作用于何处
\itemsep4pt plus 2pt minus 1pt\item 在下一条目前加入的空白
\parsep4pt plus 2pt minus 1pt同一条目内的段落之间;在列表中它同时是 \parskip,故也落在条目之间
\topsep8pt plus 2pt minus 4pt整个列表上下的空白
\partopsep2pt plus 1pt minus 1pt仅当列表前有空行时才叠加到 \topsep 之上

表格最后一行描述的行为,几乎每个人都要栽一次。\begin{itemize} 前面留不留空行,会让列表上方的空白在 8pt 与 10pt 之间变化:有空行时列表以垂直模式开始,而这正是 \partopsep 被加上的时刻。只是重新排了排源文件、结果版面挪了位,多半就是这个原因。这些数值本身也有讲究——它们是带 plusminus 的弹性长度,排页时可以压缩到 6pt,也可以拉伸到 12pt。写在源码里的数字并不会原样出现在纸面上。层级更深时默认值还会自动收紧:第二层的 \itemsep\parsep 都是 2pt,第三层的 \parsep 降为 0pt。嵌套列表显得更紧凑,正是这样设计的。

latex
% tighten one list by hand: inside the body it is \parskip,
% not \parsep, that still carries the second half of the gap
\begin{itemize}
  \setlength{\itemsep}{0pt}
  \setlength{\parskip}{0pt}
  \item first
  \item second
  \item third
\end{itemize}

请注意这里归零的是 \parskip 而不是 \parsep。从 \parsep\parskip 的复制在 \begin{itemize} 执行完时就已经完成,所以进入正文之后再改 \parsep,后半那 4pt 依然留着。实测下来,默认从一条到下一条的 19.93pt,只把 \parsep 归零只能降到 15.94pt,唯有把 \parskip 归零才会变成 11.96pt——恰好一行。与其每次重复这套写法,不如加载 enumitem 并传入 [noitemsep](把 \itemsep\parsep 归零)或 [nosep](在此之上再把 \topsep\partopsep 归零);再用 \setlist 就能把它们设为全文默认。与此相关的另一类抱怨是列表老在别扭的地方跨页。这是设计而非意外:article 类把 \@itempenalty 设成了 −51。负的惩罚值说的不是「这里允许断页」,而是「这里希望断页」,所以 LaTeX 会主动把两个条目之间的接缝当作理想的分页点。不希望被拆开的列表,必须明确摁住——放进 minipage,或者用 \samepage 拴在一起。

与 \item 有关的两个报错

itemize 不能为空。若 \begin{itemize}\end{itemize} 之间一个 \item 也没有,编译就会停在 ! LaTeX Error: Something's wrong--perhaps a missing \item.。实际上,这条报错更多不是因为「忘了写 \item」,而是因为把正文或图片直接写在了列表开头。请把环境刚打开的那个位置视为保留位:那里只能放 \item,或者间距与长度的设置。反向的失误同样存在。在任何列表之外使用 \item,会得到 ! LaTeX Error: Lonely \item--perhaps a missing list environment.——通常是多写了一个 \end{itemize},或者复制了含 \item 的片段却漏掉了它的 \begin。两种情况的提示都直说了原因,所以只要顺着行号检查前后 \begin\end 是否成对即可。

什么时候该用项目符号,什么时候不该

itemize 不是「删掉连接词的散文」,而是让条目并排可比的工具。没有编号本身就是对读者的一句声明:这四项之间没有先后,也没有高下。若把范围、语体、长度都参差不齐的条目排在一起,读者就会去寻找一条并不存在的比较轴。反过来,一旦顺序或优先级开始有意义,列表就该换成 enumerate;一串暗地里编码了次序的圆点,是最难读的那种。下面是重读自己草稿时的一份检查清单。

  • 保持语法平行。 第一条若是名词短语,其余也都用名词短语;若一条是祈使句,就全部用祈使句。
  • 条目变长就退回段落。 如果每条都超过三句话,那已经是带小标题的正文,而不是列表。
  • 最多嵌套两层。 LaTeX 允许四层,但读者通常只跟得上两层。
  • 顺序一有意义就换成 enumerate 一旦想写「首先」「接着」,那就是编号列表的信号。
  • 只在有含义时才换符号。 \item[$\star$] 是用来把某一条挑出来的,不是用来装饰的。