你的 PDF 第一页上印出的目录,并不是刚才那次编译写下的。\tableofcontents、\listoffigures、\listoftables 各自读取上一次 LaTeX 编译留下的一个小文件——.toc、.lof、.lot——而这些文件不是文本缓存,而是短小的程序:每条目一行,由下一次运行去执行它们。抓住这一点,围绕这三条命令的种种困惑几乎都能迎刃而解:为什么新文档的目录是空的,为什么第一次的页码不对,为什么节标题里的脚注偏偏在第二次编译时炸掉,以及为什么 LaTeX 从不提醒你「页面上的目录已经过时了」。
.toc、.lof、.lot 里到底写了什么
一行一条,全都是 \contentsline 命令。三种列表共用同一套机制:\tableofcontents 负责 .toc,\listoffigures 负责 .lof,\listoftables 负责 .lot,文件名都与主文件相同。\contentsline 接受四个参数——条目类型、要印出的文字、页码,以及链接目标。第四个在纯 LaTeX 下是空的;载入 hyperref 后,它会填上诸如 section.1.1 这样的 PDF 目标。所以 .toc 不是目录的草稿,而是交给下一次运行的指令序列。
% one \contentsline per entry: unit, text, page, link target
\contentsline {chapter}{\numberline {1}Body}{5}{chapter.1}%
\contentsline {section}{\numberline {1.1}Short form}{5}{section.1.1}%
% and in mydoc.lof, written by \caption inside a figure:
\addvspace {10\p@ }
\contentsline {figure}{\numberline {1.1}{\ignorespaces Short caption}}{5}{figure.1.1}%不过,这些行并不会直接写进目录文件。它们先以 \@writefile{toc}{...} 的形式积存在 .aux 里,直到 \end{document} 时 LaTeX 关闭并重新读入 .aux,才流进 .toc。这条弯路带来两个实际后果。其一,打开写入流的正是 \tableofcontents 本身,因此文档里若没有这条命令,根本不会生成 .toc——条目只是留在 .aux 中。其二,由于写入是在最后一次性完成的,\tableofcontents 放在哪里都行。把它放在末页,照样得到一份完整的目录,连它上方的标题也一个不落。
| 命令 | 写出的文件 | 条目来源 |
|---|---|---|
\tableofcontents | .toc | 从 \chapter 到 \subparagraph 的各级标题,以及 \addcontentsline{toc}{...} |
\listoffigures | .lof | figure 环境内的 \caption;若给了短的可选参数则用它 |
\listoftables | .lot | table 环境内的 \caption;机制与 .lof 完全相同 |
\addcontentsline | 你指定的扩展名 | 手写的一行;页码取该时刻的 \thepage |
\addtocontents | 你指定的扩展名 | 插入的是素材而非条目:空白、格式命令 |
目录为什么是空的——以及 LaTeX 为何从不警告
因为第一次运行时根本没有 .toc 可读。日志里会出现一行 No file mydoc.toc.,而 \tableofcontents 只排出标题就往下走。文件是在那次运行结束时才写出的,所以内容要到第二次才上纸。而第二次运行时,目录本身开始占据页面,其后的页码随之移位,有时要到第三次才稳定下来。一次运行负责存储信息,另一次负责取回——这与 \label 和 \ref 的两段式机制完全同理,细节留给交叉引用页面。
还有一半很少有人提:LaTeX 从不对这个滞后发出警告。 未定义的引用会给出 LaTeX Warning: Label(s) may have changed. Rerun to get cross-references right.,但那是因为标签会与 .aux 中的上一次取值作比对。目录没有这道比对。纸面上印出的目录与刚写好的 .toc 不一致时,日志里什么都不会说——去查一次输出了空目录的运行日志,一条警告也找不到。这正是应当把次数交给 latexmk 之类构建工具、而不是自己数的理由:它会一直重复,直到 .toc 不再变化。
同样的沉默还会以更糟的形式出现。把稿件从 report 改成 article 后,旧的 .toc 里仍留着 \contentsline {chapter}{...} 这样的行。article 并未定义 \l@chapter,而 \contentsline 只是调用 \csname l@chapter\endcsname,未定义的名字会悄悄变成 \relax,于是标题和页码被当作正文原样倒进目录里。既无错误也无警告,只剩下类似「1 Alpha2」这样莫名其妙的一行。凡是在更换文档类或调整目录结构之后觉得目录坏掉了,最快的办法就是删掉 .toc(连同 .aux)再重新编译。
tocdepth 只有一个——那个会清空图目录的设置
tocdepth 是一个计数器,指定目录中要印出的最深层级:\setcounter{tocdepth}{1} 到节为止,{2} 则到小节。默认值在 article 中是 3,在 book、report 中是 2。但它并不只属于目录。翻开 article.cls 和 book.cls 会看到 \l@figure 是 \@dottedtocline{1}{1.5em}{2.3em}——也就是说,图目录的每一条都按层级 1 排版,而 \l@table 是它的别名。\@dottedtocline 拿来比较的,正是三种列表共用的那唯一一个 tocdepth。
后果相当刁钻。在 book 中若认为「目录只列章」而写下 \setcounter{tocdepth}{0},图目录和表目录就会变成空的。.lof 里条目一条不少,但层级 1 超过了取值为 0 的 tocdepth,于是一行也不印。而且不会报错。补救很简单:把 \listoffigures 放进一个分组,在组内把 tocdepth 调高。至于 tocdepth 是在读回文件时才生效——正因如此,改变深度从不需要重新生成 .toc,只要多跑一次即可——这一点由文档结构页面讲解。
\setcounter{tocdepth}{0} % contents: chapters only
% ... but this alone would print an EMPTY list of figures.
% Raise the depth for the float lists only:
\begingroup
\setcounter{tocdepth}{1}
\listoffigures
\listoftables
\endgroup
% Because the .toc is a program, a depth change can also be
% injected into the middle of it, taking effect from here on:
\addtocontents{toc}{\protect\setcounter{tocdepth}{1}}进目录的是短的那个——\section[...] 的可选参数
写在方括号里的短标题会进入 .toc,花括号里的长标题只出现在正文。写成 \section[短标题]{在页面上铺陈开来的长标题},正文里的标题依然是长的,而目录和页眉用的是短标题。\caption[短题注]{冗长的说明} 遵循同样的规则,进入 .lof、.lot 的是短的那个——题注一侧的细节留给图片题注页面。这里要紧的是:这个可选参数并不是用来「让版面好看些」的奢侈品。
标题内容会被写出到 .toc——先倒进文件,下一次运行再读回来。因此若在标题里放入脆弱命令,比如 \section{带注的标题\footnote{注}},第一次编译毫无怨言地通过,第二次却在读回文件的那一刻崩塌。先是 Runaway argument?,接着 ! Paragraph ended before \contentsline was complete.,再是 ! Argument of \@sect has an extra }.——这些消息看上去与标题毫无关系,真凶却是刚写进 .toc 的那个脚注。错误迟到一次运行,原因和目录迟到一次运行完全相同。处方就是可选参数:写成 \section[带注的标题]{带注的标题\footnote{注}},脚注便不会进入 .toc,从此不再出事。
% the bracketed form is what lands in .toc, .lof and the running head
\section[Short form]{A long section title that would wrap in the contents}
% fragile material belongs in the braces only, never in the file
\section[Title with a note]{Title with a note\footnote{note text}}
\begin{figure}
\includegraphics{plot}
\caption[Short caption]{A long caption explaining every detail}
\end{figure}「标题会被用在三处」这一事实,在载入 hyperref 之后会换个面孔重新出现。标题还会被拿去做 PDF 书签,而书签是纯文本,容不下数学公式。写 \section{$\mathcal{A}$ 的性质} 就会得到 Package hyperref Warning: Token not allowed in a PDF string (Unicode),公式被悄悄丢掉。出口是 \texorpdfstring{$\mathcal{A}$}{A}:一份交给排版,一份交给字符串,这由 hyperref 页面讲解。
把星号标题送进目录:addcontentsline 以及放在哪里
在标题紧接之后放一行 \addcontentsline{toc}{section}{引言}。\section* 和 \chapter* 没有编号,也不往 .toc 里写任何东西,所以要让它们出现在目录里,只能自己注入这一行。三个参数都是必需的。
ext— 目标辅助文件的扩展名:目录用toc,图目录用lof,表目录用lot。unit— 条目类型。在toc中是part、chapter、section、subsection等,会套用该层级的格式与缩进;lof用figure,lot用table。text— 要列出的文字。前置\protect\numberline{}可让标题与编号条目对齐;任何脆弱命令前都要加\protect。
放在哪里决定结果。翻开 latex.ltx 里的定义,\addcontentsline 不过是写出 \contentsline{unit}{text}{\thepage}{}——也就是把该行执行那一刻的页码原样烙进去。由于 \chapter* 会另起一页,若不小心把这行放在 \chapter* 之前,目录里记下的就是上一页的页码。实验结果毫不含糊:放在前面的条目指向第 2 页,紧接其后的条目指向第 3 页。读者翻到那一页,却发现那里并没有这一章。页码由 LaTeX 自动补上,所以不要自己写进 text。
% right: the line runs after the page break that \chapter* causes
\chapter*{Acknowledgements}
\addcontentsline{toc}{chapter}{Acknowledgements}
\section*{Introduction}
\addcontentsline{toc}{section}{Introduction}
% \addtocontents injects material, not an entry
\addtocontents{lof}{\protect\vspace{2ex}}另一个 \addtocontents{ext}{text} 插入的是素材而非条目。它只取目标扩展名和要写入的内容两个参数,也不附带页码。打开 .lof 看看,你会找到一行 \addvspace {10\p@ }——每逢换章,LaTeX 自己就用同样的手法注入空白。一言以蔽之:带页码的条目行用 \addcontentsline,空白与格式用 \addtocontents。 两者写入的都是给下一次运行看的内容,所以像 \vspace 这样的脆弱命令需要 \protect。
把这些列表本身放进目录:tocbibind
只需一行 \usepackage{tocbibind},图目录、表目录、参考文献和索引就会自动出现在目录里。这些标题都没有编号——在 article 中是 \section*,在 book、report 中是 \chapter*——所以放着不管永远不会出现。你也可以手工排出一串 \addcontentsline,但遇到跨多页的参考文献或索引时位置很容易搞错,交给宏包更稳妥。
默认情况下,它连目录本身也列进目录,所以大多数人最先去找的就是 nottoc 选项。排除用的选项共五个:nottoc、notlof、notlot、notbib、notindex。反过来,传入 numbib、numindex 会把参考文献和索引排成带编号的章或节,而不是无编号标题。TeX Live 2024 里附带的 tocbibind 是 2010 年的 v1.5k,出自 Peter Wilson 之手——与 tocloft 同一作者。
% list everything except the contents itself
\usepackage[nottoc]{tocbibind}
% the manual equivalent, one line per list
\cleardoublepage
\addcontentsline{toc}{chapter}{\listfigurename}
\listoffigures用 tocloft 打磨缩进、字体与点线引导
载入 \usepackage{tocloft} 之后,每一层级都能单独设定缩进、编号宽度、字体和点线引导。命令名很有规律:表示层级的前缀(part 用 toc,chapter 用 chap,section 用 sec,subsection 用 subsec,图用 fig,表用 tab)加上作用即可。缩进与编号宽度可一并指定,如 \cftsetindents{section}{1.5em}{2.5em};当编号变宽以致撞上标题时,把第三个参数调大。字体则分开控制条目标题(\cftsecfont)和它的页码(\cftsecpagefont)。
点线引导藏着一个从名字看不出来的机关。点的间距由长度 \cftdotsep(默认 4.5)决定,值越小越密,越大越疏。而用来去掉引导线的 \cftnodots 并不是开关:在 tocloft.sty 里它就是数字 5000——间距宽到一行里放不下任何一个点。同样的机关解释了一个你见过一千遍却没留意的细节:\cftpartdotsep 与 \cftchapdotsep 的默认值就是 \cftnodots,所以标准目录里只有部和章那几行没有点线。
| 命令 | 控制内容 | 写法 |
|---|---|---|
\cftsetindents | 该层级的缩进与编号宽度 | \cftsetindents{section}{1.5em}{2.5em} |
\cftsecfont | 节条目标题的字体 | \renewcommand{\cftsecfont}{\bfseries} |
\cftsecpagefont | 节条目页码的字体 | 章则用 \cftchappagefont |
\cftsecleader | 节条目的点线引导 | 替换其中的 \cftdotfill{\cftdotsep} |
\cftdotsep | 点的间距;默认 4.5,越小越密 | \renewcommand{\cftdotsep}{2} |
\cftnodots | 数值 5000——宽到放不下任何点的间距 | 用来彻底去掉引导线 |
\cftloftitlefont | 图目录标题本身的字体 | 目录则用 \cfttoctitlefont |
\usepackage{tocloft}
\renewcommand{\cftsecfont}{\bfseries}
\renewcommand{\cftsecpagefont}{\bfseries}
\renewcommand{\cftsecleader}{\bfseries\cftdotfill{\cftdotsep}}
\renewcommand{\cftdotsep}{2} % tighter dots
\cftsetindents{section}{1.5em}{2.5em} % indent, number width
% drop the leader on section lines altogether
\renewcommand{\cftsecleader}{\cftdotfill{\cftnodots}}当 tocloft 不够用时:titletoc 与 etoc
tocloft 调整的是既有行的尺寸与字体,而 titletoc 和 etoc 改写的是行的结构本身。titletoc(Javier Bezos 编写,与 titlesec 同属一套)的核心是 \titlecontents:逐层定义行前素材、编号的排法、标题、通向页码的填充,以及行后的内容。若只需要普通点线,还有简写 \dottedcontents。此外,用 \startcontents、\printcontents、\stopcontents、\resumecontents 可以在章首放上只属于该章的局部目录。若文档的标题样式本就由 titlesec 打造,目录也能用同一套语汇来统一。
etoc(Jean-François Burnol 编写)走得更远,用「行样式」与「整体样式」两层框架把目录整体重新设计。它的看家本领是 \localtableofcontents:从同一个 .toc 中可以反复取出各章的局部目录;到了这一层,连树状目录之类的花样也进入了射程。作为决策顺序,实践中这三步很好用:先用 tocloft 调好尺寸与字体;需要改变一行的组装方式时转向 titletoc;想接管目录本身的设计时再上 etoc。