TeX 主题选讲:字符

此篇基本上是译文,原文是 Victor Eijkhout 所著的《TeX by Topic》的第三章。译文项目参见:CTeX-org/tex-by-topic-cn
转载请保留本段文字,尊重原作者和译者版权。
由于原著使用 GFDL,故而本文也被传染地同样使用 GFDL 许可,而不是本站默认的 BY-NC-SA 4.0 许可。

TeX 在其内部使用字符编码来表示字符;本章讨论字符编码及相关命令。

所涉命令编目:

  • \char: 显式指定所要排版的字符。
  • \chardef: 将一个控制序列定义为一个字符编码的同义词。
  • \accent: 放置重音符号的命令。
  • \if: 测试字符编码是否相等。
  • \ifx: 测试字符编码与分类码是否都相等。
  • \let: 将一个控制序列定义为一个记号的同义词。
  • \uccode: 对于给定的字符编码,查询或设置其对应的大写变体的编码。
  • \lccode: 对于给定的字符编码,查询或设置其对应的小写变体的编码。
  • \uppercase: 将普通文本(<general text>)转换为大写形式。
  • \lowercase: 将普通文本(<general text>)转换为小写形式。
  • \string: 将一个记号转换为一个字符串。
  • \escapechar: 将控制序列转换为一串字符记号时,用作转义字符的字符之编码。在 IniTeX 中默认为 92(\)。

字符编码

在概念上说,认为 TeX 内部直接对字符进行处理是最简单的;但是实际上 TeX 内部处理的是整数,即字符编码

不同系统中,字符的编码方式可能不同。因此,TeX 使用其自身定义的字符编码方案。任何从文件或用户终端读入的字符,都会依据编码表转换成字符编码。之后,TeX 会根据这一字符编码为它们分配分类码。编码表基于 7-位 ASCII 编码表,可编码 128 个字符。

反引号({,或称左引号)可将字符(准确说是字符记号)转换为其字符编码。在 TeX 中,所有需要 <number> 的地方,都可以通过这种方式将一个字符记号的编码传给 TeX:反引号加字符或是反引号加单字符控制序列。例如 \count\a\count`\a 都表示 \count97

第二种方式在某些情况下是有必要的,例如在下例中,若去掉 \,就会让 TeX 产生误解。

1
\catcode`\%=11\quad or\quad \def\CommentSign{\char`\%}

例如在下例中,=11 会被认为是注释。

1
\catcode`%=11

单字符的控制序列可以由任何分类码的字符组成。

译注:特别地,可以是无效字符。

当字符记号被转换为字符编码后,字符编码就与字符的形状没有关联了。当然,对于大多数字符来说,在视觉上,其排版输出与输入是一致的。例如输入 a 会排版输出字符「a」。不过,即使在常见的符号中,也有例外情况。在计算机现代罗马字体中,就没有小于号和大于号。

为了使 TeX 在输出端也不依赖具体的机器,dvi 文件格式也是用这种字符编码:操作码 $n=0\ldots127$ 表示:「从当前字体中取出第 $n$ 个字符。」在 TeXprogram 中可以找到 dvi 文件的操作码的完整定义。

字符相关的控制序列

有许多种方式可以用控制序列来表示字符。例如说,\char 命令可以指定一个用于打印的字符;\let 命令可引入一个与指定字符记号同义的控制序列——包括其字符编码和其分类码。

表示要排版的字符:\char

字符可以通过数值来表示,比如 \char98。该命令会将当前字体中编码为 98 的字符添加到正在构建的水平列表中。

相较而言,用八进制或十六进制表示字符编码可能比用十进制更方便。在数值前加上单引号,即可使用八进制数:\char'142;在数值前使用双引号,即可使用十六进制数:\char"62。注意,连续使用两个单引号,比如 \char''62 是错误的写法——将两个单引号替换为一个双引号的工作是由可视化处理器承担的,而不是数字扫描操作(执行处理器)。

由于可用反引号获取字符的编码,在当前字体的编码与 ASCII 兼容时,用 \char`b\char`\b 也可以得到 “b” 这个字符。

表面上看,\char 命令与 ^^ 替换机制类似:两种机制都是通过间接的方式来访问字符。但是,^^ 替换机制发生的非常早——在 TeX 的输入处理器为字符分配分类码之前;而 \char 命令则在可视化处理器中起作用。从效果上看,\char 是告诉 TeX:「排版字体中第 $n$ 个字符。」

\chardef 命令可定义一个控制序列定义作为一个字符编码的代替品。它的用法是

1
\chardef<control sequence><equals><number>

此处的 <number> 可以是显式给出的数字,也可以是计数器值,也可以是用反引号提取得到的字符编码(见上文)。在 plain TeX 中,就有类似下面的用法:

1
\chardef\%=`\%

它等价于下面的定义方式:

1
\chardef\%=37

如此定义之后,控制字符 \% 就变成了 \char37 的同义词。也就是说,它可以用于排版第 37 个字符(通常是百分号)。

\chardef 命令定义的控制序列可作为 <number> 使用。在诸如 \newbox 的分配类命令中,就用到了这一特性。用 \mathchardef 命令定义的记号同样可以这样使用。

隐式字符记号:\let

另一种定义表示字符的控制序列的方式是使用 \let 命令,并且将字符记号置于可选等号的右边:

1
\let<control sequence><equals><token>

如此,得到的控制序列称为隐式字符记号。

在 plain TeX 中有对左右花括号定义同义控制序列:

1
\let\bgroup={ \let\egroup=}

如此,得到的控制序列被称为「隐式花括号」。

通过 \let\chardef 将字符赋值给控制序列是有区别的。被 \let 定义的控制序列是字符编码与分类码这一组合的替代品。

例如在下例中,\b 关闭了 \m 定义所在的分组,因此 TeX 会提示错误:「未定义的控制序列」。

1
2
3
\catcode`|=2 % make the bar an end of group
\let\b=| % make \b a bar character
{\def\m{...}\b \m

又例如在下例中,\b 的定义在分类码修改之前,因此它无法承担分组结束符的作用,只能表示一条竖线(或是其他任何在当前字体中编号为 124 的字符)。因此,这里构造的是一个不闭合的分组。

1
2
3
\let\b=| % make \b a bar character
\catcode`|=2 % make the bar character end of group
{\def\m{...}\b \m

前一个例子表明,即是花括号本身被重定义了(例如在排版 C 语言代码时被定义成活动字符),分组开始和结束的功能依然可以由控制序列 \bgroup\egroup 来承担。

隐式字符记号的行为与真实字符记号非常相似。举例来说,在如此定义之后,

1
\catcode`|=2 \let\b=|

以下两个真值测试均为真。

1
\if\b|
1
\ifcat\b}

再举一例。在 plain TeX 中,有如下对上标和下标符号的定义:

1
\let\sp=^ \let\sb=_

如此一来,即使键盘上没有这两个符号,也可以通过这两个控制序列得到数学环境的上标和下标。

1
x\sp2\sb{ij}\quad gives\quad $x\sp2\sb{ij}$

若是编写格式的人的键盘上也没有这些键,那么就要用更深的技巧了:

1
2
{\lccode`,=94 \lccode`.=95 \catcode`,=7 \catcode`.=8
\lowercase{\global\let\sp=, \global\let\sb=.}}

至于它为什么能起作用,可详见后文关于 \lowercase 的介绍。由于我们无法键入 ^,因此 TeX2 中的 ^^ 表示法无法在此解决问题。考虑到上标和下标的字符编码以十六进制表示分别是 5e5f,利用 TeX3 中扩展的表示法可以依照如下方式解决问题:

1
2
{\catcode`\,=7
\global\let\sp=,,5e \global\let\sb=,,5f}

使用 \meaning 命令可以查看使用 \let 定义的命令究竟代表哪个字符。例如下列代码将给出 “the character 3”。

1
\let\x=3 \meaning\x

重音

重音符号可用水平命令(<horizontal command>\accent 给出。

1
\accent<8-bit number><optional assignments><character>

其中,<character> 是分类码为 11 或 12 的字符,或是形如 \char`<8-bit number=""> 的命令,抑或是一个由 \chardef 定义的记号。此时,重音符号会被放置在该字符的上方。若 <character> 不是上述四种情况之中的任意一种,则重音符号会被 \char 命令直接处理,从而给出一个「悬在半空」的重音符号。可选的 <optional assignments> 可用于改变重音和字符的字体。

\accent 命令后必须紧跟 <character>。这一规定避免了将重音符号置于连字之上或是置于另一个重音符号之上。在诸如印度语及越南语的少数语言中,确实存在双重音符号的情况;因此这种规定有时可能会令人不爽。不过,将重音符号置于另一个之上可以在数学模式中实现。

添加重音符号不会改变字符的宽度。对于重音符号摆放的位置,TeX 假设字体文件中,重音符号摆放的高度与字母 x 的高度相适应;对于其他高度的字母,TeX 则可以通过向上或向下移动恰当的高度来调整重音符号的位置。

在 TeX 中没有真正的下重音,而是将它们视作位置较低的上重音实现的。更准确的做法是:编写一个宏,测量紧跟着的字符的尺寸,而后相应地升高或降低重音符号的位置。plain TeX 中的变音宏 \c 就是这样做的。不过,对于有下伸部分的字母,它不会降低重音符号的位置。

译注:所谓下伸部分,指的是个别小写字母比其它小写字母更低的部分。比如「p」、「q」的「小尾巴」。

重音符号的水平位置由 \fontdimen1每点倾斜slant per point)控制。为此,TeX 会引入铅空以校正重音符号的水平位置。注意,尽管这些铅空是自动插入的,但这些铅空隶属显式铅空。因此,它们会抑制铅空前后的连字符。

译注:铅空是活字印刷排版时代,排版工人用于调整字符水平位置而使用的空白铅块。

以下代码能将水平列表转储出来,作为铅空和重音符号的示例。

1
2
\setbox0=\hbox{\it \`l}
\showbox0

其结果是

1
2
3
4
5
6
\hbox(9.58334+0.0)x2.55554
.\kern -0.61803 (for accent)
.\hbox(6.94444+0.0)x5.11108, shifted -2.6389
..\tenit ^^R
.\kern -4.49306 (for accent)
.\tenit l

注意,TeX 首先放置重音符号,以保证最后一个字符的倾斜校正仍然有效。

字符真值测试

使用 \if 可检测两个字符记号的字符编码是否相等:

1
\if<token$_1$><token$_2$>

TeX 遇到 \if 后,会对其后的记号持续展开,直至遇见两个不可展开的记号。而后,不管分类码如何,若这两个记号的字符编码相等,则真值测试为真。

对于 TeX 来说,不可展开的控制序列的字符码是 256 而分类码为 16(因此控制序列的字符编码只与控制序列的字符编码相等),除非控制序列被 \let 定义成一个非活动字符记号——如前所述,此时该控制序列的字符编码和分类码与相应的字符相同。

后续章节会介绍用于检验分类码的 \ifcat 命令;此处关注真值测试

1
\ifx<token$_1$><token$_2$>

则检验字符编码和分类码是否均相等。特别注意的是,TeX 不会展开 \ifx 之后的记号。不过,若它们是宏,则 TeX 会检验它们的展开是否相等。

对于 \chardef 定义的数量,则可以用 \ifnum 来检验。

1
\chardef\a=`x \chardef\b=`y \ifnum\a=\b % is false

这是因为由 \chardef 定义的记号可被当做是数字来使用。

大写和小写

大写和小写的编码

每个字符编码都有对应的大写编码小写编码。它们可分别用

1
\uccode<number><equals><number>

1
\lccode<number><equals><number>

来指定。在 IniTeX 中,a..zA..Z 的大写编码为 A..Z 而小写编码为 a..z。其余所有的字符的大写编码和小写编码均为零。

大写和小写命令

\uppercase{...}\lowercase{...} 命令会遍历参数记号列表,而后将所有对应大写编码或小写编码不为零的显式字符替换成相应的大写编码或小写编码,分类码则维持不变。

\uppercase\lowercase 命令的参数是普通文本(<general text>),其定义如下:

此处,左花括号可以是隐式字符记号,但右花括号必须是分类码为 2 的显式字符记号。在寻找左花括号时,TeX 会进行宏展开。

\number\string 不同,大小写转换不是宏展开,而是在执行处理器中执行的。下列尝试得到 \A 的代码会因 \uppercase 不可展开而报错——TeX 将在 \uppercase 之前插入 \endcsname

译注:这里的表述比较奇怪。事实上,TeX 要求 \csname\endcsname 之间的内容展开到底后不能有不可展开的原语;而 \uppercase 正是一个不可展开的原语,故而报错。这里提出 TeX 在 \uppercase 之前插入 \endcsname,原因可能是 TeX 在发现问题「尝试修复的过程中」,会做这样的操作。

1
\expandafter\csname\uppercase{a}\endcsname

正确的写法如下:

1
\uppercase{\csname a\endcsname}

下例对 \uppercase 的使用是正确的,它可用于检测一个字符是否为大写:

1
\def\ifIsUppercase#1{\uppercase{\if#1}#1}

大写检测也可以这样做:\ifnum`#1=\uccode`#1

首字母大写(首字母的字符编码与其 \lccode 不同)的单词是否允许截断连字取决于 \uchyph 参数。若改参数为正数,则允许对首字母大写的单词进行截断连字。

关键字的大写与小写形式

TeX 关键字可以是大写形式也可以是小写形式。比如 pt 还可以写作 pTPt 以及 PT。此处,TeX 不使用 \uccode\lccode 表来确定小写形式;而是直接在大写字母的字符编码上加 32 来得到其小写形式——ASCII 编码中,同一字母大小写形式之差为 32。这种做法与 TeX 在处理非罗马字符时的具体实现方式有些关联;详见 TeXbook 第 370 页。

巧用 \uppercase\lowercase

利用 \uppercase\lowercase 不会改变分类码的特性,可以构造出一些其它方式难以构造的字符编码-分类码配对。

再将一个稍有些不同的例子。考虑这一问题(已由 Rainer Schopf 解决):给定一个计数器 \newcount\mycount,如何将其值作为字符编号输出到终端?解法如下。

1
\lccode`a=\mycount \chardef\terminal=16
1
\lowercase{\write\terminal{a}}

此处,\lowercase 命令有效地将 \write 命令的参数,从「a」转换成目标值。

字符相关编码

每个字符编码都有一系列与之相关的 <codename>。这些整数的取值范围各不相同;它们决定了在不同场合下,TeX 会如何处理该字符,或是决定了在不同场合下,该字符会如何影响 TeX 自身。

这些编码的名字罗列如下:

  • \catcode: 4 位数字(<4-bit number>,0–15);字符的分类码。
  • \mathcode: 15 位数字(<15-bit number>,0–"7FFF"8000);它决定 TeX 在数学环境下如何处理该字符。
  • \delcode: 27 位数字(<27-bit number>,0–\hex7$\,$FFF$\,$FFF);它决定 TeX 在数学环境下如何处理出现在 \left\right 之后的该字符。
  • \sfcode: 整数;它决定 TeX 如何处理位于该字符之后的空距。
  • \lccode\uccode: 8 位数字(<8-bit number>,0–255);如前所述,这是字符的小写及大写的编码。

将记号转换为字符串

\string 命令能将其下一个记号展开为由若干字符组成的字符串。例如,下例将输出 \control

1
\tt\string\control

又例如下例将输出 $

1
\tt\string$

注意,字符串操作位于记号化之后。因此下例不能输出注释符号 %,因为早在输入处理器中,注释符号就被删除了。于是,下例中的 \string 会将下一行的第一个记号转换为字符串。

1
\tt\string%

\string 命令由展开处理器执行。因此,除非显式地禁止,它必然会被展开。

输出控制序列

上例使用了打字机字族。这是因为,在计算机现代字体中,罗马字族里没有反斜线这一字符。 不过,TeX 也可以使用其它字符来打印控制序列:具体来说,TeX 使用 \escapechar 保存的字符编码对应的字符来打印控制序列。在使用下列命令时,也会用到 \escapechar 这个宏保存的值: \write\message\errmessage\show\showthe 以及 \meaning。若 \escapechar 的值为负或者大于 255,则转义字符不会显示出来;此外,在 IniTeX 中 \escapechar 的默认值是 92,即反斜线的字符编码。

译注:\tt 是 plain TeX 风格的字体选择命令。在 LaTeX 中,应当使用 \ttfamily 或者 \texttt 代替。

\write 语句中,有时可用 \noexpand 代替 \string

\string 输出输出字符串的分类码

\string 命令输出的字符串中,各个字符的分类码均为 12,唯独被字符串化的控制序列中的空格,其分类码是 10。由于控制序列内部不存在分类码的说法,\string 输出的所有空格必定只是空格字符,也就是说,它的字符编码是 32。由于 TeX 的输入处理器会将所有空格记号的字符编码设置为 32,「滑稽空格」出现在控制序列中的机会相当渺茫。

在分类码方面,其他命令的行为与 \string 表现一致:\number\romannumeral\jobname\fontname\meaning 以及 \the


您的鼓励是我写作最大的动力

俗话说,投资效率是最好的投资。 如果您感觉我的文章质量不错,读后收获很大,预计能为您提高 10% 的工作效率,不妨小额捐助我一下,让我有动力继续写出更多好文章。


撰写评论

写了这么多年博客,收到的优秀评论少之又少。在这个属于 SNS 的时代也并不缺少向作者反馈的渠道。因此,如果你希望撰写评论,请发邮件至我的邮箱并注明文章标题,我会挑选对读者有价值的评论附加到文章末尾。