跳转至

TEX学习笔记

欢迎加入本人建的"LuaTeX ConTeXt 学习互助"群:431714622,互助学习LuaTeX ConTeXt LaTeX相关技术,以及通过Lua、Python实现格式化文本、数据的自动排版。

主要学习如下资料的笔记:

  • TeX by Topic, A TeXnicians Reference, by Victor Eijkhout, 2019
  • The TeXbook, by Donald E. Knuth, 2021
  • TeX Reference Manual, by David Bausum, 2002

盒子 boxes#

数字#

  • \number,转换一个数字⟨number⟩()为十进制样式。
  • \romannumeral,转换一个正数⟨number⟩为小写罗马样式。
  • \ifnum,测试两个数的关系。
  • \ifodd,测试一个数字是奇数。
  • \ifcase,枚举case语句。
  • \count,计数寄存器前缀。
  • \countdef,定义一个与\count寄存器同义的控制序列。
  • \newcount,分配为使用的\count寄存器。
  • \advance,算术命令,从一个数字变量⟨numeric variable⟩加或减。
  • \multiply,算术命令,乘以一个数字变量。
  • \divide,算术命令,除一个数字变量。

数字和⟨number⟩#

在TEX中,⟨number⟩严格指整数。 而一般所说“数字”,有时指字符串形式的数字。

尺寸Dimensions和胶Glue#

在TEX中,垂直和水平的空格(space)可以通过 "拉伸 "(stretching)或 "收缩 "(shrinking)来调整自己。一个可调整的留白被称为胶。

  • \dimen,尺寸寄存器前缀。
  • \dimendef,定义一个与寄存器\dimen同义的控制序列。
  • \newdimen,分配一个未使用的尺寸寄存器。
  • \skip,Skip寄存器前缀。
  • \skipdef,定一个与寄存器\skip同义的控制序列。
  • \newskip,分配未使用的skip寄存器。
  • \ifdim,比较两个尺寸。
  • \hskip,插入一个水平胶。
  • \hfil,等于\hskip 0cm加1fil。
  • \hfilneg,等于\hskip 0cm加-1fil。
  • \hfill,等于\hskip 0cm就1fill。
  • \hss,等于\hskip 0cm加1fil减1fil。
  • \vskip,插入一个垂直胶。
  • \vfil
  • \vfill
  • \vfilneg
  • \vss
  • \kern,在当前水平或垂直列表中增加一个字间。
  • \lastkern,如果当前列表最后一个项目是字间,取其尺寸。
  • \lastskip,如果当前列表的最后一个尺寸是胶,取其尺寸。
  • \unkern,如果当前列表最后一个项目是字间,将其移除。
  • \unskip,如果当前列表最后一个项目是胶,将其移出。
  • \removelastskip,宏,附加负的\lastskip。
  • \advance
  • \multiply
  • \divide
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
\newdimen\columnA                    % initialise variable
\columnA=10mm                        % set variable

Column A is \the\columnA{} wide.     % lengths are automatically 
                                     % displayed in pt.

Column A is \number\columnA sp wide. % internally, lengths are stored
                                     % as an integer number of sp

\blackrule[width=\columnA]

胶⟨glue⟩和尺寸⟨dimen⟩的定义#

尺寸余论#

胶余论#

坏度 badness#

拉伸或收缩一个列表时,TEX会计算坏度,基于实际拉伸量与行中表示的拉伸总量之间的比例。坏度用于段落算法。

一个列表的坏度公式:

\[b=min(1,000,000*(\frac{实际拉伸或收缩的总量}{可能的拉伸或收缩总量})^3)\]

为效率起见,实际算法有简化。

\badness记录了最近构造的盒子的坏度。

胶和断breaking#

TEX可以在一个胶之前断行、断页,然后此胶被丢弃。

在Plain TEX中有两个宏,\hglue和\vglue,分别在水平和垂直模式下提供不消失的胶。对于水平的情况,实际放置的是:

\vrule width 0pt \nobreak \hskip ...

因为TEX在胶的前面断开,这个胶会一直附着在条线(rule)上,因此不会消失。实际的宏定义要复杂一些,因为它们要注意保留 \spacefactor 和 \prevdepth。

字间 \kern#

字间相当于不能伸缩的胶,但不是合法断点。比如:

.. text .. \hbox{a}\kern0pt\hbox{b}

以上盒子间不能断开;

.. text .. \hbox{a}\hskip0pt\hbox{b}

以上盒子间可以断开。

不过,如果字间后跟一个胶,字间处可以断开(假设不是数学模式)。在水平模式中,字间和胶都在断开时消失。在垂直模式下,断点前的材料被输出例程处理,它们被移动到(空的)当前页,于是被丢弃。

胶与模式modes#

所有水平skip命令都是⟨horizontal command⟩,所有垂直skip命令都是⟨vertical command⟩。这就是说,比如\hskip用在垂直模式中,TEX就会开始一个新段落。\kern命令可以用在两种模式中。

列表尾胶:尾空backspacing#

列表中的最后一个胶是可以测量的,可以移除,除非在外部垂直模式下。内部变量 \lastskip 和 \lastkern 可以用来测量所有模式下的最后一个胶;如果最后一个胶不是skip或kern,它们分别给出0pt。在数学模式下,\lastskip的功能是⟨internal muglue⟩,但在一般情况下,它归类为⟨internal glue⟩。

如果测的是列表最后的glue或kern,\lastskip和\lastkern也是0pt。

\unskip和\unkern操作,分别移除列表最后的glue和kern。它们在外部垂直模式下无效;此时最好的替代方法是\vskip-\lastskip和\kern-\lastkern。

在段落构造过程中,TEX自身执行一个重要的\unskip: 一个以空白行结尾的段落会被TEX的输入处理器插入一个空格token。在插入 \parfillskip 胶之前,它将被 \unskip 移除。

胶会被TEX当做铅条(leader)的一个特例,对铅条应用\unskip时自然会删除铅条。

尾空的例子#

宏\removelastskip的定义是:

\ifdim\lastskip=0pt \else \vskip-\lastskip \fi

如果列表的最后一个条目是胶,这个宏会用它的值加尾空,加入它的自然尺寸不是0。否则,什么也不加。

跟踪输出中的胶#

  • \tracingoutput
  • \showbox
  • 当overfull或underfull时

盒子的呈现形式(比如用\showbox)会在显式kern后加空格,但不会在字体tfm文件插入的隐式kern后加空格。所以,“\kern 2.0pt”表示用户或宏插入的kern,“\kern2.0pt”表示隐式kern。

比如:

\vbox{\hbox{Vo}\hbox{b}}

跟踪输出为:

\vbox(18.83331+0.0)x11.66669 .\hbox(6.83331+0.0)x11.66669 ..\tenrm V ..\kern-0.83334 ..\tenrm o .\glue(\baselineskip) 5.05556 .\hbox(6.94444+0.0)x5.55557 ..\tenrm b

段落形状#

\hsize 用来排版段落的行宽,包含\leftskip、\rightskip、\parindent、\hangafter(n行后悬挂)、\hangindent等。

\parshape⟨equals⟩ n i1 ℓ1 . . . in ℓn,针对前n行每行的缩进(i)和行长度(l)。多余n行时,重复使用最后一组数据。少于n行时,忽略多余数据。指定\parshape后\hangindent无效。

断行#

"坏度"(badness):TEX用这个概念来决定如何将一个段落分成几行,或者在哪里断页。

各种罚点,如连字,形成断行成本。

  • \penalty,罚点,表示不在此处断行的意愿强度。
  • \linepenalty 与每次断行相关的罚点。默认为10。
  • \hyphenpenalty 通常情况下,在一个连字符处断行的罚点。默认值为50。
  • \exhyphenpenalty,当断点前文本为空时,在水平行连字符条目处分行的罚点。默认50。
  • \adjdemerits,毗连行的视觉不协调的罚点。默认10000。
  • \doublehyphendemerits,连续以连字符结束的行的罚点。默认10000。
  • \finalhyphendemerits 倒数第二行有连字时加入的罚点。默认5000。
  • \allowbreak,插入\penalty0,生成断点。
  • \pretolerance 一个无连字符段落的坏性容忍的值。默认100。
  • \tolerance 一个无连字符段落中的每行的坏性容忍值。默认为200。
  • \looseness 超过理想行数的行数(可以是负值)。
  • \prevgraf 最近加入垂直列表的段落的行数。

\prevgraf 必须仅仅在两次断行之间有效,断行时/在段内则被重置,无法取值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
% 段首绕排时判断行数够不够,加足够的空行(或vskip),以免重叠
\define[3]\zi{%
    \startnarrower[left]%
        \newcount\hanginglinesnum \hanginglinesnum=2%
        \setupindenting[yes,-1em]%
        \starthanging[location=left,n=\hanginglinesnum]{\tfd \bf #1}
        #2 \par%
        \advance \hanginglinesnum by -\prevgraf%
        \dorecurse{\hanginglinesnum}{\blank}%
        \stophanging%
        \setupindenting[yes,2em] #3%
    \stopnarrower%
}

当盒子、图片、MP在行首时会自动切换到v模式,从而导致自动断行。可以前置 \dontleavehmode or \quitvmode 以阻止这种行为(\nobreak\penalty10000等是事后加胶以控制断行算法,此处无效); 或cheat:前置0宽胶\hskip\zeropoint or \hskip0pt

另参考\leavevmode \ifvmode

段落的断行成本计算#

分行构造一个段落,为的是与断行相关的总扣分d最小。一个段落的总扣分值是各个行的扣分值的总和,再加上一些可能的额外罚点。将一个段落作为一个整体来考虑,而不是逐行断开,可以导致更好的断行:TEX可以选择在段落的开始部分取一个稍微不那么漂亮的行,以避免以后出现更大的麻烦。对于每一行的扣分是由拉伸或收缩到断行处的坏度b和与断行有关的罚点p计算出来的。坏度不允许超过某个预先设定的容许值。除了对单个行的断行进行扣分外,TEX还对行的组合方式进行扣分。

TEX段落分行算法的实现,参见:D.E. Knuth and M.F. Plass. Breaking paragraphs into lines. Software practice and experience, 11:1119–1184, 1981.

坏度/坏性 Badness#

根据行中表示的拉伸或收缩与实际使用的拉伸或收缩之间的比值,算出在某一点断行的坏度。坏度是断行的重要因素。

坏度也用于断开垂直列表,这里仅讨论断行。

描述坏度的术语:

  • tight (3),紧,指任何收缩行坏度 b ≥ 13,即至少使用了收缩量的一半。
  • decent (2),正,指任何行的坏性 b ≤ 12。
  • loose (1),松,指任何拉伸行坏度 b ≥ 13,即至少使用了拉伸量的一半。
  • very loose (0),很松,指任何拉伸行坏度 b ≥ 100,即尽数或超额使用了拉伸量。请记住,胶可以拉伸超过许可量,但不能收缩超过许可量。

该计数用于跟踪输出,也用于以下定义:如果相邻两行的分类号数相差超过1,则称这两行视觉上不协调(visually incompatible)。与此相关的\adjdemerits参数见下文。

如果发生溢出的水平盒子和垂直盒子的宽度或高度分别小于 \hfuzz 或 \vfuzz,则忽视;如果坏度小于 \hbadness 或 \vbadness,则不报告。

罚点Penalt和其他断开位置#

水平列表的可能断点(breakpoints):

  1. 在罚点penalty处。罚点是此处断行的美学代价(aesthetic cost)。负罚点被看做红利。罚点10000或更多,则禁止断行;罚点−10000或更少,则强制断行。连续放置一个以上的罚点,相当于只放置一个最小值的罚点,因为这个罚点是断行的最佳候选。水平模式下的罚点是由用户(或用户宏)插入的。唯一的例外是在\parfillskip胶之前插入的\nobreak。
  2. 在胶glue处,如果胶不是数学公式的一部分,并且前面有一个不可丢弃(non-discardable)项。在胶处断行是没有惩罚的。不可丢弃的前项这个条件是必须的,否则可能在两个胶之间断行,这将导致段落的边缘出现锯齿。
  3. 在字间kern+胶glue处, 如果字间不是数学公式的一部分。在字间处断行没有罚点。
  4. 在数学关(math-off)+胶glue处。因为“数学关”(和“数学开”,math-on)用作字间,这很像上一条。在“数学关”处断行没有罚点。
  5. 在词语断字(discretionary break)处。其罚点是\hyphenpenalty或\exhyphenpenalty。 后当论及。

任何可丢弃材料,跟随在断点——胶、字间、数学开/关、罚点——之后时,将被丢弃。如果你知道在胶(kern, math-on/off)处断行实际发生在胶的前端,这就意味着这个胶在断行中会消失。

缺点 Demerit#

假设l是\linepenalty值,b是行的坏性,p是断点的罚点,那么行缺点d:

\[d=\begin{cases} (l + b)^2 + p^2 & if & 0 \le p < 10000 \\ (l + b)^2 - p^2 & if & -10000 < p < 0 \\ (l + b)^2 & if & p \le -10000 \\ \end{cases}\]

段落的缺点是各行缺点的总合,再加上:

  • 所有\adjdemerits(毗连行不协调罚点)
  • 所有\doublehyphendemerits(连续断词行罚点)
  • 和\finalhyphendemerits(倒数第二行断词罚点)

段落的行数#

\prevgraf,可以与悬挂缩进、段落形状等联合使用,以影响TEX的决定过程。

行间#

段落机制把行打包为水平盒子,然后添加到垂直列表中。结果的垂直条目序列如下重复:

  • 一个盒子,包含一行文本;
  • 可能的迁移来的垂直材料;
  • 一个罚点条目,反应在此处断页的代价,通常是\interlinepenalty;
  • 行间胶,自动计算自\prevdepth。

断行处理#

TEX试图以这样的方式来分割段落,即每行的坏性(badness)不超过一定的容忍值(tolerance)。如果存在一个以上的解决方案,那么就取其缺点(demerits)最少的一个。

通过设置\tracingparagraphs为正值,可以使TEX在日志文件中报告段落机制的计算结果。一些TEX的实现可能会禁用这个选项以使TEX运行得更快。

三遍#

一遍,尝试不不断词的断行成段,如果每一行的坏性都没有超过\pretolerance 则成功。

否则二遍,插入断词断点,应用\tolerance。如果\pretolerance是负值,则忽略一遍。

如果一、二遍都失败,还可以让TEX做第三遍。如果\emergencystretch是正值尺寸,TEX将应用这么多的额外拉伸可能性于每一行,据此计算坏性和缺点。哪些较少超过容忍值的解决方法,此时被选用。不过,并没有使用一个胶来表达\emergencystretch的尺寸,因此盒子不满的信息可能会出现。

容忍值#

TEX排版一段文字时会遇到多少麻烦,部分基于容忍值。因此明智的是,稍微了解坏性在视觉术语中的意义。

对于拉伸后的行,坏性是100乘以拉伸率的立方。因此坏性800意味着,拉伸率是2。 如果空格如下,以10点Computer Modern字体为例,

3.33pt plus 1.67pt minus 1.11pt

坏性800意味着所有空格被拉伸为:

3.33pt + 2 × 1.67pt = 6.66pt

就是说,结果正好是2倍自然尺寸。由你判断这是不是太大了。

段落结束#

TEX会在段落的最后一个元素后隐式插入\parfillskip的等价物:

\unskip \penalty10000 \hskip\parfillskip

\unskip用于移除段末的假胶,比如由行尾生成的空白,当\par由输入处理器插入时。

\parfillskip在纯TEX中是一级无限胶(first-order infinite,0pt plus 1fil),所以段末是\hfil$\bullet$\par时,bullet将处于最后一个词语到行末的半中间;段末是\hfill$\bullet$\par时,则被冲到右边。

空格#

TODO#

\relax#

命令解析终止标记,以免继续解析造成歧义(有些命令还能接收更多参数)。

glue 胶#

TEX中的内容都以盒子的形式存在,水平盒子之间由胶连接,还有其他的元素,构成水平列表;水平列表再如此构成垂直列表。

Glue如同弹簧一样,有三个属性:自然空间(space);拉伸(stretch)能力;收缩(shrink)能力。只有当行、列版面糟糕的时候才会按比例拉伸和收缩。拉伸值最终可能被系统突破,而收缩值不会被突破。glue一旦设定(setting)后,就不变了。

  • \smallskip,等于\vskip3pt plus1pt minus1pt,或\hskip3pt plus1pt minus1pt
  • \medskip,2*\smallskip
  • \bigskip,2*\medskip
  • \vskip⟨glue⟩,glue:⟨dimen⟩ plus⟨dimen⟩ minus⟨dimen⟩,自然值必须指定,加减值默认为0
  • \hskip
    • \enskip,等于\hskip.5em\relax
  • \vfil,无限扩展胶,等于\vskip0pt plus \vsize
    • \centerline,居中,两端加无限制胶
  • \vfill、\vfilll,与\vfil共用时,拉伸强度不同
  • \vss、\hss,无限扩展和收缩胶
  • \hfil、\hfill、\hfilll
  • \hfilneg and \vfilneg取消\hfil and \vfil的效果
  • fil、fill、filll作为尺寸,\vskip 0pt plus 1fil minus 1fil

标点后的glue有特殊规定。

行间距:

  • \baselineskip=⟨glue⟩
  • \lineskip=⟨glue⟩
  • \lineskiplimit=⟨dimen⟩

空白系数 space factor f

水平列表开头的空白系数是1000,在一个非字符盒子或数学公式推送到列表上之后,也会再设置为1000。

可以这样指定\spacefactor=⟨number⟩空白系数。不过,一般是在简单字符盒子(simple character box)加入列表时才设置为非1000的值。 每个字符都有空间系数代码g,进入列表时会设置为当前空间系数。但是,如果g是0,则f不变;如果f < 1000 < g,则空间系数将设置为1000。 (也就是说f不会从1000之下一步跳到1000之上。)空间指数最大是32767。

重载字体词间距与伸缩:

  • \spaceskip,伸展f/1000倍,收缩1000/f倍
  • \xspaceskip,当前space factor f >= 2000时使用(space factor f 通常是1000)
  • \sfcode‘)=0 \sfcode‘.=3000,个别字符的空间系数指定
  • \frenchspacing重置为1000

指定盒子的伸缩(相对于自然宽度):

\hbox spread 5pt{‘‘Oh, oh!’’ ... laughed.}

\kern 字间#

字间类似glue,但不能伸缩,不会作为断行点。

\line 行#

\line{}生成宽度为\hsize的行。

\mark 标记#

主要供参考书类出版物,比如词典,用于排印页面顶端的引导词。可用的相关原语:botmark, firstmark, topmark, splitbotmark, splitfirstmark.

\insert 插入#

插入vbox材料,诸如脚注、尾注、浮动体。

protect 保护#

fragile-and-robust-commands

最佳实践:当你发现奇怪的错误出现在一个会移动的参数(即一段文本,会移动到文档的其他部分,比如脚注会移动到页脚,标题会移动到目录,等等)之中或附近,那就尝试\protect其中的所有宏。这可以解决99%的问题。

评论