LaTeX 的列表在第四层就走到了尽头。easylist 干脆放弃嵌套环境,从而越过了这堵墙:在 \begin{easylist} 里,# 的个数就是层级,写 ### 是第三层,写 ##### 是第五层。既不用 \item,也不用嵌套的 \begin。这个宏包还带了一个名叫 tractatus 的预设,它原样再现了维特根斯坦《逻辑哲学论》的编号方式。本页讲 easylist 怎么写、真正的上限在哪里(十层,以及触顶时的报错),以及什么时候它胜过 enumitem、什么时候不。所有结论都在 TeX Live 2024 上实际排版验证过。
easylist 怎么用 —— # 的个数就是层级
用 \usepackage[sharp]{easylist} 加载,然后在 easylist 环境里让每行以一串 # 开头。一个 # 是第 1 层,## 是第 2 层。标记串之后必须恰好留一个空格。这里从不写 \item——也没有地方写。因为 easylist 不建立嵌套环境,深度不取决于开了几个 \begin,而只取决于那一行带了几个 #。这正是它被称为「层数无上限」的原因,也是你可以在列表中的任意位置随意上下跳层的原因。
\documentclass{article}
\usepackage[sharp]{easylist}
\begin{document}
\begin{easylist}
# First proposition.
## Interesting comment.
### A note on the comment.
### Another note.
#### By the way...
# Let's start something new.
\end{easylist}
\end{document}在 TeX Live 2024 上排版这段,编号依次是 1. 1.1. 1.1.1. 1.1.2. 1.1.2.1. 2.:各层计数器用句点连接,最后一个后面也带句点。那个末尾句点就是 FinalMark,可以去掉。跳层时被跳过的层记作 0:在第 1 层项目后直接写 ###,编号就是 1.0.1.,并不报错。但只要漏掉标记后的空格,它就会悄悄出错。 写 #With no space. 时该项目完全没有编号,而随后的 ## 行则变成了 0.0.1.——不只是丢了标号,之后所有计数器都错位了。
真正的上限是十层 —— 出现 Too many 报错时
「层数无上限」并不完全属实。默认只创建十层计数器,写到第十一层编译就会中断。在 TeX Live 2024 上跑一份十四层的列表,实际报出 ! Package easylist Error:,随后是三行以 ==> Too many 开头的说明,大意是:除非在加载宏包时另行指定,否则标记最多只能用十个。而且它仍然生成的 PDF 里,第十一层起的每个项目都在本该是编号的位置带着字面文本 !!!。若在日志里漏看了这个错误,印出来的就是一串带三个惊叹号的列表。
解决办法正是错误信息说的:把层数作为数字选项在加载时传入。\usepackage[sharp,14]{easylist} 让同一份源文件顺利通过,一路排到 1.1.1.1.1.1.1.1.1.1.1.1.1.1. 而无报错。数字可以任意,但计数器是在加载时分配的,所以只申请你需要的层数才算得体。另外,如果你真的写出了超过十层的列表,还有一句显而易见的忠告:该被质疑的多半是文档结构本身。
从 tractatus 到 itemize:六种内置预设
easylist 自带六种预设,在环境的可选参数里指名,如 \begin{easylist}[tractatus]。它们是 tractatus、checklist、booktoc、articletoc、enumerate 和 itemize,定义在 easylist.sty 第 820 行附近。其中第一个正如其名,就是维特根斯坦《逻辑哲学论》的编号体系。实际排出来是 1. 1.1 1.11 1.2 2.:只有第一层后面有句点,更深的层数字径直连写下去。那本 1921 年著作的目录,一个选项词就能拿回来。
| 预设名 | 排出的编号或记号 | 适用场合 |
|---|---|---|
tractatus | 1. 1.1 1.11 1.2 2. | 命题分层排列的哲学与逻辑文本 |
checklist | 隐藏编号,每项前放一个空方框 | 供手工打勾的核对表 |
booktoc | 第 1 层大号粗体加大写罗马数字,更深层依次缩进 | 手工排的目录 |
articletoc | booktoc 的轻量版:第 1 层粗体,编号后不加句点 | 论文规模的目录 |
enumerate | 依次是 1. (a) i. A. | 想深挖但外观与标准 enumerate 一致 |
itemize | 依次是 • – ∗ · | 不带编号的项目符号,模仿标准记号 |
其中 enumerate 与 itemize 两者是刻意模仿标准列表的外观。实测下来,前者排出 1. (a) i. A.,后者排出 • – ∗ ·。但 itemize 预设并没有使用 \labelitemi 之类真正的标号命令,而是用 \textbullet、--、$\ast$、$\cdot$ 手工凑出的近似(easylist.sty 第 858–864 行)。标准记号究竟如何定义,custom-labels 那一页有实测;若你想改动记号本身,那里才是正途。easylist 的价值在于保持这套外观的同时能一路走到第五层、第六层。
用 \ListProperties 决定编号与缩进
预设不够用的地方,交给 \ListProperties(...)。在圆括号里写以逗号分隔的 key=value 对,设置会作用于其后的所有项目和列表。许多键有按层的形式,只需在键名后附上层号:Numbers 管所有层,Numbers3 只管第 3 层。\NewList 把一切恢复默认。要注意 \ListProperties 不是必需的——不写它 easylist 照样能跑,本页第一个例子就是如此。设置只是在你想改外观时才需要。
| 键 | 作用 | 写法示例 |
|---|---|---|
Numbers | 编号种类:a 阿拉伯数字(默认)、r/R 小写或大写罗马数字、l/L 小写或大写字母、z Zapf Dingbats | Numbers2=R, Numbers3=l |
Mark / FinalMark | 计数器之间的分隔符(默认句点)与最后一个计数器之后的记号 | FinalMark= 去掉末尾句点 |
Style | 字体;加 * 只作用于编号,加 ** 只作用于正文 | Style1*=\bfseries 只让第 1 层的编号加粗;也可传入 \color{blue} 之类 |
Hide | 隐藏前 n 个计数器,由此得到不带编号的列表 | Hide=10000 隐藏所有层的编号 |
Start | 计数器起始值;带 * 的形式与外部计数器联动 | Start1*=\thesection 跟随节号 |
Progressive | 左边距随深度递增;带 * 的形式让第 1 层留在当前边距 | Progressive=1.5em |
Space | 项目之间的垂直间距;带 * 的形式指同层项目之间。负值可收紧 | Space*=2pt |
\documentclass{article}
\usepackage[sharp]{easylist}
\begin{document}
\ListProperties(Style1*=\bfseries, Numbers2=R, Numbers3=l,
Progressive=1.5em, Space*=2pt)
\begin{easylist}
# Agreement.
## Definitions.
### Party means a signatory hereto.
### Effective Date means the date of last signature.
## Term and termination.
### This Agreement begins on the Effective Date.
#### Either party may terminate on 30 days notice.
##### Notice must be given in writing.
# Miscellaneous.
\end{easylist}
\end{document}这里第 1 层排出粗体阿拉伯数字,第 2 层大写罗马数字,第 3 层小写字母,组合成 1.I.a.。第 4、5 层只需写 #### 和 ##### ,由 Progressive 各自再缩进 1.5em。其余用于微调的键还有:Hang(让正文悬挂于编号)、Align(对齐同层项目的开头)、Margin 与 Indent(左边距与段落缩进)、FinalSpace(编号与正文之间的间隔,默认 .3em)以及 CtrCom(用 \fbox 之类把整个编号包起来)。若要在值里写入原义逗号或右括号,用花括号裹住:\ListProperties(FinalMark={)})。
#、@ 还是 &:该选哪个标记,哪个会毁掉表格
标记由宏包选项选择:不写选项得到节号 §,[pilcrow] 得到 ¶,[sharp] 得到 #,[at] 得到 @,[ampersand] 得到 &。你选的那一个字符只在 \begin{easylist} 与 \end{easylist} 之间成为活动字符,出了环境就恢复原样。这里广为流传的建议——# 是 LaTeX 的参数字符,会在表格里冲突,换成 & 更安全——实测下来恰恰相反。
在 TeX Live 2024 上把 tabular 放进 easylist 环境里试,[sharp] 毫无问题地通过了;在环境外定义的宏,在环境内照常调用也没事。但换成 [ampersand] 做同样的事,& 就被当作项目标记而非列分隔符吃掉:! Missing } inserted.、! Missing \cr inserted.、! Missing { inserted. 接连出现,表格单元格直接变成了第 2、3、4 个列表项。对含表格的列表来说,& 是最糟的选择。真正会让 # 出问题的,是在环境内部定义新宏。把 \newcommand\inside[1]{[#1]} 放在 # \inside{boom} 之前,#1 会被读成标记,定义根本不成立,于是报 ! Undefined control sequence.。请在环境外定义宏,或在列表内用 \Activate 与 \Deactivate 临时关掉标记。
| 选项 | 标记 | 实测行为 |
|---|---|---|
(none) | §(节号,默认) | 唯一的缺点是键盘上不好打 |
pilcrow | ¶(段落记号) | 与 § 相同,同样不好打 |
sharp | # | 在 tabular 里也能通过,但环境内不能定义宏。写 \# 仍可打出字面字符 |
at | @ | 在 beamer 的 frame 里不带编号,@ 被原样印出。正文中含 @ 的文档应避开 |
ampersand | & | 在含 tabular 的列表里是灾难:报 ! Missing \cr inserted.,表格变成列表项 |
beamer 里编号消失 —— 忘了 [fragile] 的后果
要在 beamer 幻灯片里用 easylist,请给 frame 加上 [fragile]。加了之后,[sharp] 和 [at] 都按预期编号。不加的话,两种标记的症状截然不同。[sharp] 会大张旗鼓地失败,吐出 ! Illegal parameter number in definition of \iterate.,以及一条抱怨宏参数字符不能用于内部垂直模式的 ! You can't use ...,项目也塌成一行。[at] 更麻烦:退出码为 0,一个错误都没有,可 PDF 里却原样印着 @ One. @@ Two.。悄无声息的失败才是后患。
\documentclass{beamer}
\usepackage[sharp]{easylist}
\begin{document}
% [fragile] is required: without it the markers are tokenised before
% easylist has a chance to make them active.
\begin{frame}[fragile]{Nested points}
\begin{easylist}
# First point.
## Supporting detail.
\end{easylist}
\end{frame}
\end{document}另一个常见担忧是它是否与 enumitem 冲突。并不冲突。 在 TeX Live 2024 上,先加载 enumitem 或后加载都能零错误编译,同一份文档里 easylist 的列表与 \begin{itemize}[label=\textbullet] 和平共处。二者职责并不重叠:enumitem 用键值选项重塑 itemize 和 enumerate 的外观,而 easylist 是在它们之外另添一个环境。同时加载不会带来麻烦,至少在加载阶段不会。
easylist 与 enumitem,该用哪个
一个问题就能定夺:深度本身是文档的结构,还是仅仅是外观问题? 合同条款、标准文件、法规、层层分叉的大纲——凡是五层六层本身就承载意义的文本——答案是 easylist。多加一个 # 就深一层,写长条文时不必在脑子里数 \begin 与 \end 的配对,这份轻松是实打实的。反过来,若列表最多两三层,你要的是对编号格式、行距、行内列表和标号替换的精细控制,那么 enumitem 才是正道。以键与值搭建的设计思想归它所有,easylist 的 \ListProperties 替代不了。
最后一点。标准的 itemize 与 enumerate 各自持有独立的深度计数器,交替嵌套便能挖到六层——这一事实由 custom-labels 那一页实测。所以那道著名的「四层墙」并没有看上去那么高。easylist 依然值得一用,是在超过六层时,或是你厌倦了数环境配对的那一天。它出自 Paul Isambert 之手;TeX Live 2024 同捆的是 v1.4,日期为 2014 年 1 月 11 日,采用 LPPL 许可。这是一个十年未动的宏包——但正因为做的事少,能出问题的地方也少。