enumitem (格式、计数器、行内)

LaTeX 的列表太松散——人们求助于 enumitem 宏包,多半就是为此,可真正查过数字的人不多。在 article 类中,两个条目之间的基线距离是 20pt,而普通行距只有 12pt——每两个条目之间硬是多出 8pt 的空白。这 8pt 由两个长度构成:\itemsep(4pt)与 \parsep(4pt)。enumitem 宏包的作用,正是让人用 itemsep=0pt 这样的 键=值(key=value)选项 直接改写它们。本页逐一核对默认值究竟是多少、nosepleftmargin=* 到底改了什么,以及如何用 \setlist 为整篇文档定下基调——所有数字均为实测。

LaTeX 列表的默认值到底是多少

article 类(10pt、第 1 层)中实测:\itemsep 为 4pt,\parsep 为 4pt,\topsep 为 8pt,\leftmargin 为 25pt,\labelwidth 为 20pt,\labelsep 为 5pt。有了这些数字,"太松散"的抱怨就变得具体了。两个条目之间插入的垂直空白是 \itemsep\parsep合计 8pt——加上 12pt 的行距即为 20pt。于是在按 12pt 网格排布的正文里,唯独列表以每项 1.67 行的间距排出。就排版而言这是有意的设计:条目本就该在视觉上彼此分开。但十个短条目连排,页面就会显得空荡。横向也是同样的故事:\leftmargin 有 25pt,而圆点本身还不到 5pt 宽,其中 20pt 纯属留白。

长度(即键名)article 默认值(第 1 层)作用位置
itemsep4pt(plus 2pt minus 1pt)相邻两个条目之间
parsep4pt(plus 2pt minus 1pt)同一条目内的段落之间
topsep8pt(plus 2pt minus 4pt)整个列表的上方与下方
partopsep2pt(plus 1pt minus 1pt)仅当列表另起一段时额外加入
leftmargin25pt整个条目区块相对左侧的缩进
labelwidth20pt放置标签的盒子宽度;更宽的标签会向左溢出
labelsep5pt标签盒与条目正文之间
rightmargin0pt右侧边距;想让列表左右两侧同时收窄(如引文)时使用
itemindent0pt仅条目首行的额外缩进;取负值可使其向左伸出
labelindent—(enumitem 新增的键)从外侧到标签盒左缘的空白

层级越深,数值越小。itemize 第 2 层的 \itemsep\parsep 降到 2pt,\topsep 降到 4pt,\leftmargin 降到 22pt;第 3、4 层的 \parsep 已经是 0pt,\topsep 只剩 2pt,\leftmargin 则细到 18.7pt 与 17pt。可见 LaTeX 从一开始就抱定"越浅的层级留白越多"的方针,深层嵌套本来就是紧凑的。人们真正想压缩的往往只有第 1 层,原因就在这里。

键=值的写法——键名就是 LaTeX 的长度本身

在导言区写 \usepackage{enumitem} 载入宏包后,itemizeenumeratedescription 三个环境便获得了一个 可选参数:在方括号内以逗号分隔写下若干 key=value,这个列表的版式就定下了。这套设计的关键在于,键名与上表的长度名完全一致itemsep=1ex 改写 \itemsepleftmargin=1cm 改写 \leftmargin。因此学会 enumitem 就等于学会了 LaTeX 列表的内部机制——相较于旧时用 \renewcommand 覆盖内部命令的做法,这才是它真正的长处。TeX Live 2024 随附的是 Javier Bezos 的 v3.9(2019/06/20);用 kpsewhich enumitem.sty 可以查看本机所用的版本。

document.tex
\usepackage{enumitem}
% ...
\begin{itemize}[itemsep=1ex, leftmargin=1cm]
  \item the key names are the LaTeX length names
  \item second entry
\end{itemize}

同一个键可以在三层结构中被覆盖:整篇文档、某一类环境、单个列表(借助下文的 \setlist)。最具体的值胜出,因此稳妥的做法是先定下全局方针,只在必须之处局部退出。另外,若某个值整体被花括号包住,最外一层会被剥掉(这是 keyval 的默认行为),所以想保留花括号本身时请写成双层。

压缩列表——nosep 与 noitemsep 的区别

nosep四个 垂直长度全部归零,noitemsep 只归零 两个。查阅宏包源码可见,nosep\partopsep\topsep\itemsep\parsep 四者一并设为 0pt,而 noitemsep 仅设 \itemsep\parsep。最容易被忽略的是 partopsep,那是列表在空行之后(另起一段)开始时才追加的 2pt。实测加了 nosepitemize,条目基线间距恰好是 12pt——与正文的普通行距完全一致。列表上下的空白也一并消失,于是彻底融入行文之中。

document.tex
% nosep  -> partopsep = topsep = itemsep = parsep = 0pt
% noitemsep -> itemsep = parsep = 0pt, list keeps its 8pt topsep
\begin{itemize}[nosep]
  \item item baselines land exactly 12pt apart
  \item as tight as ordinary body text
\end{itemize}

选哪一个,取决于这份列表是否要读作行文的一部分。若条目本身就是主角——操作步骤、幻灯片——用 nosep 无妨。但嵌在论文正文里的短列表,noitemsep 更稳妥:上下的 8pt 得以保留,段落与列表的分界因而清晰可辨。若连上下也一并压掉,列表就会黏在前后段落上,读者难以看出列举从哪里起、到哪里止。此外还有一个旧键 nolistsep,它与 nosep 的唯一区别是把 topsep 设为 0pt plus .1pt 而非彻底的零;新写的文档请用 nosep

label= 与 ref=——分别决定标签与交叉引用

列表的标签由 label= 决定。值当中的 带星号 形式 \arabic*\alph*\Alph*\roman*\Roman* 代表该层当前的计数器。label=(\arabic*) 得到 (1) (2) (3),label=\alph*) 得到 a) b) c),label=\Roman*. 得到 I. II. III.。带星形式不接受参数,可以把它们看作只在标签内部才有意义的特殊写法。此外还有 label*=,它把编号接在父级标签之后,嵌套时便累积为 1.、1.1.、1.2. 这样的形式。

document.tex
\begin{enumerate}[label=(\arabic*), ref=\arabic*]
  \item \label{step:one} prepare the ingredients
  \item heat the pan
\end{enumerate}
Go back to step~\ref{step:one}.   % prints "step 1", not "step (1)"

默认情况下,label= 的设定也波及交叉引用的外观。若某个列表只写了 [label=(\arabic*)],给其中一项加 \label\ref,正文里出现的是带括号的 (1)——"回到步骤 (1)"里括号重复的日常烦恼正由此而来。补上 ref=\arabic*,标签仍是 (1),而 \ref 的返回值就变成了光秃秃的 1。将上面的代码实际排版验证,结果正是如此。另需注意,labelref 的值按移动参数处理,其中若含脆弱命令,可能需要 \protect;带星号的计数器写法本身则无妨。

若想以旧 enumerate 宏包那样的简写来写标签,载入时用 \usepackage[shortlabels]{enumitem}。此后在方括号里把 AaIi1 写作 第一个元素,便分别等同于 \Alph*\alph*\Roman*\roman*\arabic*,于是 \begin{enumerate}[(1)] 也能用了。移植旧稿脱离 enumerate 宏包时这很方便,但新写的文档还是明确写出 label= 为好,日后重读时意图更清楚。

leftmargin=* 与 wide——让左边距自己算出来

横向的键并非彼此独立,而是由一条等式绑在一起:\leftmargin + \itemindent = \labelindent + \labelwidth + \labelsep。因此,给其中某些键传 * 而非具体数值,enumitem 便会解出等式的其余部分。实测效果颇为可观:默认 itemize\leftmargin 是 25pt,加上 [leftmargin=*] 后缩到 9.99pt——恰好只留住圆点本身的宽度。反过来的情形更要紧:遇到 label=(\roman*) 这类较宽的标签,"(viii)" 塞不进默认的 20pt 盒子,会向左溢出;而补上 leftmargin=*,盒子就随标签一同变宽。用宽标签时一并写上 leftmargin=*,记住这一条便能少出许多岔子。

另一个便利的键是 wide。它把左边距与标签盒的宽度都归零,使标签读起来像正文的一部分,条目则排成普通段落的样子。查看源码可知,它把 \leftmargin\labelwidth 设为零,将参数(默认为 \parindent)放入 \labelindent,令 \listparindent 与之一致,再由等式算出 \itemindent。实测结果是 \leftmargin 为 0pt、\itemindent 为 20pt、\listparindent 为 15pt(即 \parindent)。想改变缩进量时给它一个值,如 wide=0pt。当版面要求列表不向左突出、正文左缘保持齐整时,这个键正好派上用场。与之相配的还有 align=,它决定标签在盒内是左对齐(left)、右对齐(right,默认)还是放进左对齐的 parbox(parleft)。

从 5 开始编号、接着上一个列表续编(start、resume、series)

start= 指定首项的编号,resume 则接着紧邻的上一个 enumerate 继续编号。在步骤中间插入一段说明、之后从"7."接着往下写,是极常见的需求;若靠手写 \setcounter 来做,一旦中途补进一个步骤,编号立刻错位。实际排版验证:[start=5] 排出 5 和 6,中间隔一段文字后 [resume] 果然从 7 接续。改用 resume*,则不只继承编号,连 上一个列表的选项设置 也一并承接(这个带星的写法只在环境的方括号里才有意义)。

document.tex
\begin{enumerate}[start=5]
  \item numbered 5
  \item numbered 6
\end{enumerate}
An explanatory paragraph goes here.
\begin{enumerate}[resume]
  \item numbered 7 -- the count carries over
\end{enumerate}

resume 只看紧邻的上一个列表,因此当彼此相隔甚远的多个列表——比如跨越若干节——需要共用一套连续编号时,它就不够用了。这时应当使用 系列:给第一个列表挂上 series=名称,之后的列表写 resume=名称resume*=名称,无论中间隔着什么,编号都会接续。撰写按节拆分的长篇操作手册时,这是唯一真正站得住脚的办法。

用 \setlist 作用于全文,用 \newlist 造自己的列表

每次都写同样的选项并不现实。\setlist 用于指定列表种类与层级,把设置成批地施加上去:不带方括号的 \setlist{...} 触及所有列表的所有层级,\setlist[itemize]{...} 覆盖 itemize 的全部层级,\setlist[enumerate,1]{...} 则只作用于 enumerate 的第 1 层。带星的 \setlist* 是在既有设置之上 追加 而非覆盖。若想在全文范围内修正本页开头所述的松散感,在导言区写下一行 \setlist{noitemsep} 是性价比最高的一步。

preamble
\usepackage{enumitem}
\setlist{noitemsep}                    % tighten every list in the document
\setlist[itemize]{leftmargin=*}        % let itemize size its own margin
\setlist[enumerate,1]{label=(\arabic*), ref=\arabic*}

% a list type of your own
\newlist{steps}{enumerate}{1}
\setlist[steps]{label=\textbf{Step \arabic*.}, leftmargin=*}

再进一步,还可以定义专属于自己的列表类型。\newlist{名称}{类型}{最大深度} 会克隆三种基本列表之一(enumerateitemizedescription)为一个新环境,随后用 \setlist[名称]{...} 赋予其版式(至少必须设定标签)。实际排版上面的代码,\begin{steps} 输出了"Step 1."与"Step 2."。其收益与其说在于版面,不如说在于 源文件的可读性:看到 \begin{steps},读者不必翻看成品页面就知道这是一串步骤。列表的含义由名称而非版式来承载。

排在段落之内的行内列表(enumerate*)

\usepackage[inline]{enumitem} 载入宏包后,便多出 enumerate*itemize*description* 三个 带星号的环境,其条目在段落中横向流动,而不另起行。实际排版的结果就是一行文字:"We need (1) flour (2) sugar (3) salt for this."。若只是数点三四个短语,这种写法往往比用四行版面另起一个列表更易读;在学术文章中列举条件时,这反而是更标准的写法。

document.tex
\usepackage[inline]{enumitem}
% ...
We need
\begin{enumerate*}[label=(\arabic*)]
  \item flour \item sugar \item salt
\end{enumerate*}
for this.

分隔符可用这几个键调整:itemjoin(条目之间,默认是空格)、itemjoin*(只改动最后一项之前的分隔符——英文里 "a, b, and c" 的写法正靠它)、afterlabel(紧跟标签之后,默认为 ~)。但要注意,行内列表的条目默认是装在盒子里的,因而浮动体、嵌套列表与行间公式都放不进去;确有需要时请指定 mode=unboxed。实务上稳妥的分界是:行内列表只用来排列简短的词句。