调整换行

TeX Live 2024 附带的 hyphen.tex,第一行就是一句喊话:The Plain TeX hyphenation tables [NOT TO BE CHANGED IN ANY WAY!]。这个文件收着 4447 条英语连字模式,LaTeX 每次决定在哪里换行都要查它;文件末尾还附着一份手写的例外表,总共只有十四个词。其中 presentproject 一个断点都没有——同一串字母有两种读法,TeX 分不清你指的是哪一个,索性不断。调整断行位置,说到底就是在自己的稿子里重做这个判断。本页覆盖 \\\newline\linebreak[n]\nolinebreak[n]、任意连字符 \-\hyphenation{...}hyphenat 宏包,以及 babel 的分语言连字模式,数字全部实测。

先划一条界线。用空行或 \par 另起段落是一回事,调整段落内部的断行位置是另一回事。前者带来首行缩进和段间距,后者只在同一段之内挪动行尾的位置。再往外还有第三层——版面节奏的设计,\widowpenalty\raggedbottom 的世界——那属于分页设计那一页。请求把一段排短或排长一行的 \looseness,连同实测结果也放在那里。本页只谈这一层。

\\\newline 的区别:同一个宏,不同的入口

\newline 的定义是 \@normalcr\relax,而 \\ 就是同一个宏去掉那个 \relax(见 latex.ltx)。差别就在这一个记号上。\\ 会继续去看后面有没有 *[长度]\newline\relax 挡住,根本不看。实际排一下 Alpha\newline[2ex] Beta,纸面上就会原样印出 [2ex]。反过来,\\[2ex] 会在该行之后加 2ex 的竖直间距,\\* 则禁止在该处分页。实务上的分工:要用选项就写 \\,要让源码读起来意图明确就写 \newline

两者的共同点是结束这一行而不把它拉长。宏体是 \unskip … \nobreak \hfil \break:先在右侧放一个可无限伸展的 \hfil,然后才断行。这个 \hfil 把富余全部吸走,于是行停在自然长度上,保持短。这正是它们适合地址和诗句的原因——每一行都应该在你想要的位置结束。但有一个副作用。把 \\ 放在段落的最末尾,就制造出一条空的末行,日志随即报 Underfull \hbox (badness 10000) in paragraph at lines 6--7。段落末尾不需要 \\——紧跟标题之后的那一行同样不需要。如果用在根本没有行可结束的位置(比如段落开头),得到的则是 ! LaTeX Error: There's no line here to end.

latex
% \\ and \newline end the line short; \linebreak justifies it to the margin.
First road is steep\\
second road is gentle.

% \\ takes an optional length and a star; \newline takes neither.
Alpha\\[2ex] Beta        % 2ex of vertical space after the line
Alpha\\*    Gamma        % no page break allowed here
Alpha\newline[2ex] Delta % prints a literal [2ex] on the page

% A trailing \\ at the end of a paragraph produces
%   Underfull \hbox (badness 10000) in paragraph at lines 6--7
This paragraph does not need one.

在表格里,两者的角色泾渭分明。在 tabularp{...} 列这样的会生成段落的单元格中,\newline 只在该单元格内断行,而 \\ 结束整行(整条记录)。想把地址塞进一个单元格排成两行?那就用 \newline。实测:在 p{3cm} 单元格里写 Line one\newline Line two,同一行内叠成两层,右侧相邻单元格纹丝不动。array 环境里同样由 \\ 分行。反过来,在 lc 这类不生成段落的列里用 \newline,因为没有行可结束,就会得到上面那条错误。

\linebreak[n]\nolinebreak[n] 里的 n 到底是多少惩罚值

n 取 0 到 4 五档,分别翻译成 0、51、151、301、10000 这几个惩罚值。 latex.ltx 里的 \@getpen 返回 0 / \@lowpenalty / \@medpenalty / \@highpenalty / \@M,而在标准文档类中,这三个量在 article.cls 第 118–120 行被设为 51151301\linebreak[n] 把该值取放进去(在此断行有奖励),\nolinebreak[n] 则取(在此断行要付代价)。省略参数时两者都按 4 处理,也就是 ∓10000——TeX 眼中的无穷大。看见数字,取舍就定了:[1] 背后的 51 只是一句耳语,正文的 badness 轻易就压过它;到了 [3] 的 301 才算真正开口;只有 [4] 是命令。

参数linebreak 放入的值nolinebreak 放入的值实际强度
[0]00只是一个合法断点,不赏不罚
[1]-5151一句耳语;正文的 badness 就能压过
[2]-151151\widowpenalty 的默认值同一量级
[3]-301301到这里才算真正开口
[4]-1000010000默认值;无穷大,即命令

\linebreak\\ 断行的结果相同,成品却截然相反。\linebreak 只是放下一个惩罚值,因此该行照常两端对齐,被拉伸到右边距——字间会显得空旷正是这个缘故。\\ 则用那个 \hfil 吸走富余,保持短。记住这句就不会犹豫:要断得短就用 \\,要拉齐到边距就用 \linebreak 细节上,\@no@lnbk 会先用 \unskip 去掉前面的空白,放入惩罚值,再补回同宽的空白——所以写成 word\linebreak word 也不会出现双倍空格。

不许在此断开:连接号 ~\mbox 的代价

如果只想保住一个空格,波浪号 ~ 是首选。 它的宽度与普通词间空格相同,唯一的区别是那里不能断行。凡是不能与数字或符号分离的词都用它:Fig.~3Dr.~SmithChapter~12equation~\ref{eq:1}。关键在于,~ 不会妨碍前后单词的连字。它相当于 \nobreak 加一个空格——只在那一点放一个无穷惩罚(\nobreak\penalty10000)——单词本身仍可自由断开。需要分级强度就用 \nolinebreak[n],需要一刀切的禁止就用底层的 \nobreak

\mbox{...} 则是另一回事。它把内容装进一个盒子,于是里面的东西既不能跨行断开,也不能加连字符。这适合像 T-34 这样不许拆开的短语,但要付代价。实测:在宽 4.2cm 的 minipage 里,dichlorodiphenyltrichloroethane 原样放进去能排下;一旦裹上 \mbox{...},日志就报 Overfull \hbox (18.8814pt too wide),单词越出右边距 18.88pt。把长 URL 或化合物名用 \mbox 固定,是比病更重的药。下文的 \hyphenation{...} 无断点登记,以及 hyphenat\nohyphens{...},都能在不做成盒子的前提下只关掉连字符。

\- 追加连字点:不是「这里也能断」,而是「只能在这里断」

只要在一个词里插入哪怕一个 \-,LaTeX 就只会在你标记的位置断词,绝不会用它自己找到的那些点。 由模式推出的候选全部消失。用 \showhyphens 一排就看得很清楚:supercalifragilistic 原样有五处可断,su-per-cal-ifrag-ilis-tic;一旦写成 super\-califragilistic,就只剩 super-califragilistic 这一处。同样,hyphenationhy-phen-ation 变成 hyphen-ation,只因为写了 hyphen\-ation。也就是说 \- 是对候选表的替换而非追加,你想要的每一个断点都得自己写出来。另外 \- 并不强制断行——它只表示「若要断,必须在这几处之一」。

latex
% Ask TeX where it would break, and read the answer in the .log:
%   [] \T1/cmr/m/n/10 su-per-cal-ifrag-ilis-tic
\showhyphens{supercalifragilistic}
%   [] \T1/cmr/m/n/10 super-califragilistic   <- one \- killed the other four
\showhyphens{super\-califragilistic}

% So spell out every break point you actually want:
super\-cali\-fragi\-listic

为什么 long-standing 的后半截断不开:hyphenat\hyp

因为同一条规则也适用于你自己敲下的普通连字符。\showhyphens 验证:longstanding 会断成三段 long-stand-ing;可一旦写作 long-standing,结果就只剩 long-standing——只有你敲的那个连字符还在,stand-ing 没了。所有复合词都是如此:well-knownX-raystate-of-the-art。行尾撞上长复合词时排版忽然变难,正是这条规则所致。解法是 hyphenat 宏包的 \hyp。写成 long\hyp standing,实测又回到 long-stand-ing:连字符看上去分毫未变,模式给出的候选却复活了。

hyphenat 里还有一件可以顶替 \mbox 的工具。\nohyphens{...} 的定义只有一行:把组内的 \language 切换到一门没有模式的语言。实测中 \nohyphens{supercalifragilistic} 的候选归零。因为它不造盒子,词的前后照旧可以断行——不会像 \mbox 那样越出 18pt。同样的思路下,\bshyp\fshyp\dothyp\colonhyp 把 URL 和路径里的 \/.: 变成可断字符;宏包选项 [none] 则仅用 \hyphenpenalty=10000\exhyphenpenalty=10000 两条赋值,就关掉整份文档的连字。

latex
\usepackage{hyphenat}

% A plain "-" kills the rest of the word:  long-standing
% \hyp prints the same hyphen and keeps the patterns: long-stand-ing
long\hyp standing, well\hyp known, state\hyp of\hyp the\hyp art

% No hyphens in this word, but the line may still break around it.
% Unlike \mbox this cannot overflow the margin.
\nohyphens{supercalifragilistic}

% Breakable punctuation for URLs and paths:
https:\fshyp\fshyp example\dothyp com\fshyp very\fshyp long\fshyp path

\hyphenation{...} 不起作用时:重音符、词形变化、大写

同一个词反复出现时,就在导言区登记一次例外,不必弄脏正文。写法与高德纳在 hyphen.tex 末尾所做的完全一样:把用连字符标出断点的词列进 \hyphenation{...}。这个机制还有一个从写法上看不出来的用途登记一个不含任何连字符的词,它就再也不会被断开。 实测:写下 \hyphenation{fortran} 之后,\showhyphens{fortran} 的候选归零。开头提到的 presentproject 之所以以无断点的形式列在高德纳的例外表里,正是这个道理。与 \mbox 不同,它不造盒子,因此不会越出边距。

失效的原因也实测了三条。其一是重音符。在 OT1(默认字体编码)下写 \hyphenation{r\'e-sum\'e} 会以 ! Improper \hyphenation will be flushed. 中断,因为 OT1 里的重音字符是用 \accent 拼出来的,并非字母。加上 \usepackage[T1]{fontenc} 之后它成为单个字形,同一行便悄无声息地通过。其二是完全匹配。登记 \hyphenation{hyphena-tion} 后,hyphenation 确实断成 hyphena-tion,但复数形式 hyphenations 完全对不上这条例外,退回模式给出的 hy-phen-ations。词形变化必须逐个登记。其三,不区分大小写。TeX 先按 \lccode 转成小写再匹配,所以仅 For-tran 一条,就让 FORTRANFortranfortran 分别断成 FOR-TRANFor-tranfor-tran

latex
\usepackage[T1]{fontenc}   % without this, an accent in \hyphenation errors:
                           % ! Improper \hyphenation will be flushed.

\hyphenation{% one entry per inflected form; case does not matter
  hyphena-tion  hyphena-tions
  r\'e-sum\'e
  fortran                 % no hyphens = never break this word
}

babel 的分语言连字模式:Wissenschaft 有两种断法

连字模式是分语言的,切换 \language 会让同一个词在不同位置断开。 实测说明一切。载入 \usepackage[english,ngerman]{babel}\language 变为 36(ngerman),\showhyphens{Wissenschaft Wahrscheinlichkeit} 给出正确的 Wis-sen-schaft Wahr-schein-lich-keit。可一旦切到 \selectlanguage{english}\language 为 0),同样两个词就变成 Wis-senschaft Wahrschein-lichkeit——按德语看是错的。反过来也一样:用德语模式排英文,typesetting 会断成 ty-pe-set-ting。不加 babel 写出的德语论文之所以总透着一股别扭,多半就是这个缘故。

由此直接推出一个后果:\hyphenation{...} 的例外表本身也是按 \language 分别保存的,因而只对声明当时生效的那门语言起作用。实测:在 \usepackage[english,ngerman]{babel} 之后于导言区写下 \hyphenation{Wiss-en-schaft},ngerman 正文中该词确实按指定断成 Wiss-en-schaft;可 \selectlanguage{english} 之后这条例外整个不见了,退回英语模式的 Wis-senschaft。多语言文档里有条铁律:要在选定目标语言之后再登记例外。 另外,中日文没有这个意义上的连字,因此 \-\hyphenation 都是给文档中西文部分用的工具。

latex
\usepackage[T1]{fontenc}
\usepackage[english,ngerman]{babel}

% Exceptions belong to the language that is current when they are declared.
% Select the language first, then declare.
\selectlanguage{ngerman}
\hyphenation{Wiss-en-schaft}
\selectlanguage{english}
\hyphenation{hyphena-tion hyphena-tions}

% Check the result rather than guessing:
%   ngerman -> Wis-sen-schaft   english -> Wis-senschaft
\showhyphens{Wissenschaft}
命令作用是否拉伸该行
\\ , \newline强制断行(不换段);只有 \\ 接受 [长度]*否——\hfil 吸走富余,行保持短
\linebreak[n]放入惩罚值 −(0/51/151/301/10000);默认 n=4是——该行被拉齐到右边距
\nolinebreak[n]放入惩罚值 +(0/51/151/301/10000);n=4 为彻底禁止
~不可断的词间空格;仍允许连字
\mbox{...}装进一个盒子;不断行也不加连字符——过满框的常见来源
\-任意连字符;此后该词只在标记处断开
\hyphyphenat;照常打印连字符,同时保留全词的候选断点
\nohyphens{...}hyphenat;只关掉连字符。不造盒子,因此不会溢出
\hyphenation{...}导言区的例外表;完全匹配、不分大小写、按 \language 分开
\showhyphens{...}诊断用:把该词当前的断点写进 .log

修复断行警告的顺序:力气活留到最后

最不该做的修法,就是在警告出现的地方敲一个 \\ 了事。 TeX 的断行是着眼整段做优化的,钉死一行,剩下的分配就会变,另一行随之出问题。正确的顺序是:改写原稿 → 补充可断点 → 用 ~\nolinebreak 局部禁止 → 最后才放宽整段的容差。最后这一档——\tolerance\emergencystretch\sloppy(恢复默认用 \fussy),以及如何读懂 Overfull \hbox——归「Overfull \hbox 的处理」那一页管(\sloppy 其实只是四条赋值,也在那里实测过)。用 microtype 让字形微微伸缩的做法同样属于那一页。

  • 先把长词、URL 和化合物名挑出来。 逐个送进 \showhyphens{...},看看它们究竟有没有断点。
  • 给专业术语一条 \hyphenation{...} 每个词形变化各占一条;多语言文档要写在 \selectlanguage 之后。
  • \- 留给最后那一个词。 记住,只要放一个,该词其余的候选就全没了。
  • 人名、编号、单位用 ~ 拴住。 \mbox 只留给真正绝对不能拆开的短语。
  • 多语言文档一定要载入 babel 不切换语言,总有一门语言会一直断错位置。