LaTeX
注意
在您玩 LaTeX 宏时,您会发现它的功能是相当有限的。您可能会想,您每天使用的这些包是如何在如此简单的环境中实现的。实际上,LaTeX 是一组 Plain TeX 宏,大多数包都使用 Plain TeX 代码。Plain TeX 更为底层,具有更多的功能,但代价是学习曲线陡峭且编程复杂。
除了少数例外,您可以在有效的 LaTeX 文档中使用完整的 Plain TeX 语言,而反过来则不行。
术语
为了避免混淆,似乎有必要解释一些术语。
-
组 是指从左大括号到匹配的右大括号之间的所有内容。
-
标记 是字符、控制序列或组。
-
控制序列 是以
\
开头的任何内容。它不会按原样打印,而是由 TeX 引擎根据其类型进行扩展。 -
命令(或函数或宏)是一个控制序列,它可能会扩展为文本、重新定义控制序列等。
-
原语 是 TeX 引擎硬编码的命令,即它不是写在 Plain TeX 中的。
-
寄存器 是 TeX 用来处理变量的方式。它们的数量是有限的(在经典 TeX 中,每种类型的寄存器为 256 个,在 e-TeX 中为 32767 个)。
-
胶水 是放置在盒子之间的空间量,当它们被连接时,胶水提供了空间。
可能还有更多术语,但希望这些足够清晰。
Catcodes(类别代码)
在 TeX 中,某些字符具有特殊含义,并且不是直接打印其关联的符号。例如,\
用于引入控制序列,默认情况下不会打印反斜杠。
为了区分字符的不同含义,TeX 将它们分为类别代码(catcodes)。TeX 中有 16 种类别代码。
TeX 的一个强大功能是它能够重新定义语言本身,因为存在一个 \catcode
函数,允许您更改任何字符的类别代码。
然而,这并不推荐,因为它会使代码难以阅读。如果您在类或样式文件中重新定义了任何类别代码,请确保在文件的末尾将其恢复。
如果您在文档中重新定义了类别代码,请确保在前导部分之后进行,以避免与包的加载发生冲突。
类别代码
代码 | 描述 | 默认设置 |
---|---|---|
0 | 转义字符和控制序列 | \ |
1 | 组的开始 | { |
2 | 组的结束 | } |
3 | 数学切换符 | $ |
4 | 对齐标签 | & |
5 | 行结束符 | ^^M (ASCII 返回) |
6 | 宏参数 | # |
7 | 上标 | ^ 和 ^^K |
8 | 下标 | _ 和 ^^A |
9 | 被忽略的字符 | ^^@ (ASCII 空字符) |
10 | 空格 | ␣ 和 ^^I (ASCII 水平制表符) |
11 | 字母 | A...Z 和 a...z |
12 | 其他字符 | 所有未列出的字符,尤其是 @ 。 |
13 | 活跃字符 | ~ 和 ^^L (ASCII 换页符) |
14 | 注释字符 | % |
15 | 无效字符 | ^^? (ASCII 删除) |
活跃字符
活跃字符类似于宏:它们是单个字符,在任何其他命令之前会被扩展。
\catcode`| = 13
\def|{\TeX}
...
This is a stupid example of |.
This is a stupid example of TeX.
请注意,活跃字符需要直接跟随一个定义,否则编译会失败。
示例
Texinfo
Texinfo 使用类似 TeX 的语法,但有一个主要的区别:所有函数都是以 @
而不是 \
开头。这并非偶然:它实际上使用 TeX 来打印文件的 PDF 版本。基本上,它通过输入 texinfo.tex
来重新定义控制序列字符。可能的实现:
\catcode`\@=0
@def@@{@char64} % 写 '@' 字符。
\catcode`\\=13 @def\{{@tt @char92}}
The @TeX 命令以前是写作 '\TeX'。现在写作 '@@TeX'。
此重新定义后,@
应该会引入每个命令,而 \
将实际打印反斜杠字符。
项目符号(Itemize)
一些人可能会觉得 LaTeX 列表环境的语法有点繁琐。这里是一个快速定义类似 wiki 的项目符号的方法:
\catcode`| = 13
\def|{\item {--}}
\def\itemize#1{{\leftskip = 40 pt #1 \par}}
\itemize{
| First item
| Second item
}
美元符号和数学
如果您有许多美元符号需要打印,您可以更改数学切换字符。
\catcode`$ = 11
\catcode`| = 3
It costs $100.
Let's do the math: |50+50=100|. Let's highlight it:
||50+50=100||
\makeatletter 和 \makeatother
如果您进行过一些 LaTeX 的黑客操作,您一定遇到过这两个命令,\makeatletter
和 \makeatother
。
在 TeX 中,@
字符默认属于 catcode 11(字母)。这意味着您可以将它用于宏名称。LaTeX 利用这个 catcode 来规定一条规则:所有不公开的、内部的宏(即不应被最终用户访问的宏)名称中至少包含一个 @
字符。在文档中,LaTeX 会将 @
的 catcode 更改为 12,其他字符则保持不变。
因此,当您需要访问 LaTeX 内部的功能时,必须使用 \makeatletter
和 \makeatother
将所有访问私有功能的命令包裹起来。它们仅仅是更改了 catcode:
\def\makeatletter{\catcode`@ = 11}
\def\makeatother{\catcode`@ = 12}
Plain TeX 宏
\newcommand
和 \renewcommand
是 LaTeX 特定的控制序列。它们会检查是否有现有的命令被新定义的命令所覆盖。
在 Plain TeX 中,宏定义的原语不会检查可能的覆盖情况。您需要自己确保不破坏任何已有的定义。
其语法为:
\def<macroname>#1<sep1>#2<sep2>{宏内容,使用参数 #1 等}
您可以在参数之间使用几乎任何字符序列。例如,下面我们定义一个简单的宏,将小数点分隔符从点改为逗号:
\def\pointtocomma #1.#2{(#1,#2)}
这样,当您写下:
\pointtocomma 123.456
它将打印 (123,4)56
。我们加上括号只是为了突出问题。每个参数是匹配宏定义的最短可能输入序列,包括分隔符。因此,#1
匹配所有直到第一个点的字符,#2
仅匹配第一个标记,即第一个字符,因为它后面没有分隔符。
解决方案: 添加第二个分隔符。一个空格可能看起来很方便:
\def\pointtocomma #1.#2 {(#1,#2)}
通常来说,每次您预期有多个参数并且有特定的分隔符时,最好考虑最后一个分隔符。如果您不想处理分隔符,那么 Plain TeX 宏的使用方式与 LaTeX 宏相同(没有默认参数):
\def\mymacro#1#2#3{{\bf #1}#2{\bf #3}}
%% ...
\mymacro{word1}{word2 word3}{!!!}
扩展定义
TeX 还有另一个定义命令:\edef
,表示扩展定义。语法与 \newcommand
相同:
\edef<macroname><argumentslist>{<expanded content>}
该内容会在使用 \edef
的地方扩展(但不会执行,即不会打印),而不是在宏被定义的地方。宏扩展并不总是显而易见的...
示例:
\def\intro{Example}
\edef\example#1{\intro~---~#1}
\def\intro{Exercise}
\example{This is an example}
在这里,\intro
的重新定义对 \example
没有影响。
全局定义
定义是有限制的,只在其作用域内有效。然而,有时可能需要在某个组内定义一个宏,使其在该组外依然有效,直到文档结束。这就是所谓的全局定义。
{
\def\LocalTeX{Local\TeX}
\global\def\GlobalTeX{Global\TeX}
}
I can still access the \GlobalTeX{} macro here.
您还可以在 \edef
中使用 \global
命令。
这两个命令有快捷方式:
-
\gdef
等同于\global\def
-
\xdef
等同于\global\edef
长定义
前述定义命令不允许在多个段落中使用,即包含 \par
命令或双换行的文本。
您可以在定义前加上 \long
命令,以允许多段落的参数。
示例:
\long\def\dummy#1{#1}
\dummy{First paragraph\par Second paragraph}
外部定义
此前缀宏可防止某些上下文中使用定义。它有助于整合宏并减少由于上下文不当而导致的错误。外部宏的定义用于不依赖于任何上下文的情况,因此得名。
例如,下面的代码会失败:
\outer\def\test{a test}
\def\failure{\test}
外部宏不能出现在以下情况下:
-
宏参数中
-
被跳过的条件中
-
...
-
\let
和\futurelet
中
\let<csname><token>
等同于 \expandafter\def\expandafter<csname>\expandafter{<content>}
。它定义了一个新的控制序列名称,该名称等于指定的标记。通常,标记是另一个控制序列。
注意,\let
只会扩展一次标记,而与之不同的是,\edef
会递归地扩展,直到不再有扩展为止。
示例:
使用 \let
:
\def\txt{a}
\def\foo{\txt}
\let\bar\foo
\bar % 打印 a
\def\txt{b}
\bar % 打印 b
使用 \edef
:
\def\txt{a}
\def\foo{\txt}
\edef\bar{\foo}
\bar % 打印 a
\def\txt{b}
\bar % 打印 a
\futurelet
语法
\futurelet<csname><token1><token2>...
的工作方式稍微不同。首先,token2
被分配给 csname
,然后 TeX 处理 <token1><token2>...
序列。因此,\futurelet
允许您在使用时分配一个标记。
特殊控制序列名称
有些宏的名称不能直接写出。这种情况出现在宏的名称由多个宏名称组成的情况下。示例如下:
\def\status{full}
\def\varempty{This is empty}
\def\varfull{This is full}
\csname var\status \endcsname
上面的最后一行将根据 \status
的值打印相应的句子。
此命令实际上执行了与 \string
相反的操作,\string
用于打印控制序列的名称而不进行扩展:
{\tt \string\TeX}
输出:
\TeX
控制扩展
\expandafter{token1}{token2}
将在扩展 token1
之前扩展 token2
。有时当 token2
需要扩展但由于 token1
的存在而不能扩展时,这个命令就很有用。
{\tt \expandafter\string\csname TeX\endcsname}
输出:
\TeX
\noexpand
对于精确控制哪些内容在 \edef
中被扩展非常有用。示例如下:
\def\intro{Example}
\def\separator{~---~}
\edef\example#1{\intro\noexpand\separator#1}
\example{no expand makes the separator dynamic in an {\tt \string\edef}.}
\def\intro{For instance}
\def\separator{~:~}
\example{the separator changed, but not the first word.}
控制序列
\the
控制序列将让你查看各种 TeX 类型的内容:
示例:
文本尺寸:$ \the\hsize \times \the\vsize $
寄存器
寄存器是类型化的变量。它们的数量有限,范围从 0 到 255,共有 6 种不同类型:
类型 | 描述 |
---|---|
box | 一个盒子 |
count | 一个整数 |
dimen | 一个长度 |
muskip | 一种胶水(mu 单位) |
skip | 一种胶水 |
toks | 一系列标记 |
TeX 在内部使用一些寄存器,因此最好不要使用它们。
保留寄存器列表:
-
\box255
用于页面内容 -
\count0-\count9
用于页面编号
临时寄存器(可自由使用):
-
\box0-\box254
-
\count255
-
\dimen0-\dimen9
-
\muskip0-\muskip9
-
\skip0-\skip9
使用 =
控制字符来分配寄存器。对于盒子寄存器,使用 \setbox
命令。
\count255=17
\setbox\mybox=\hbox{blah}
您可以使用以下宏来防止冲突:
-
\newbox
-
\newcount
-
\newdimen
-
\newmuskip
-
\newskip
-
\newtoks
这些宏使用以下语法:\new*<csname>
,例如:
\newbox\mybox
\setbox\mybox=\hbox{blah}
这些命令不能在宏内使用,否则每次调用宏时都会为其保留另一个寄存器。
您可以使用 \the
命令来打印寄存器。对于计数器,使用 \number
命令。对于盒子,使用 \box
命令。
\the\hsize
\number\count255
\box\mybox
算术运算
TeX 的算术能力非常有限,尽管这个基础足以扩展一些有趣的特性。主要有三个函数:
-
\advance <register> by <number>
-
\multiply <register> by <number>
-
\divide <register> by <number>
register
可以是类型为 count
、dimen
、muskip
或 skip
的寄存器。对于 box
和 toks
类型的寄存器,无法进行算术运算。
条件语句
基础语法如下:
\if* <test><true action>\fi
\if* <test><true action>\else<false action>\fi
其中 \if*
是以下命令之一。
控制序列 | 描述 |
---|---|
\if <a><b> |
如果两个字符代码相等,返回真。 |
\ifcat <a><b> |
如果两个类别代码相等,返回真。 |
\ifdim <a><rel><b> |
测量关系,可能为 < , > , 或 = 。 |
\ifeof |
如果是文件结束或文件不存在,返回真。 |
\iffalse |
总是返回假。 |
\ifhbox <reg> |
如果盒子寄存器包含水平盒子,返回真。 |
\ifhmode |
如果在水平模式下,返回真。 |
\ifinner |
如果在内部模式下,返回真。 |
\ifmmode |
如果在数学模式下,返回真。 |
\ifnum <a><rel><b> |
数字关系,可能为 < , > , 或 = 。 |
\ifodd <num> |
如果数字是奇数,返回真。 |
\iftrue |
总是返回真。 |
\ifvbox <reg> |
如果盒子寄存器包含垂直盒子,返回真。 |
\ifvmode |
如果在垂直模式下,返回真。 |
\ifvoid <reg> |
如果盒子寄存器为空,返回真。 |
\ifx <a><b> |
如果两个宏扩展结果相同,或者两个字符代码相等,或者两个类别代码相等,返回真。 |
示例:
\ifnum 5>6
This is true
\else
This is false
\fi
输出:
This is false
自定义条件语句
你可以使用 \newif
命令创建新的条件语句(类似布尔变量)。通过这些自定义条件语句,你可以优雅地控制代码的输出。以下是示例:
需要生成两种版本的文档。一种版本给 A 组,另一种给其他人(即不属于 A 组的人):
-
使用
\newif
来定义条件(即布尔变量)。
\newif\ifgroupA
-
通过如下方式为条件赋值(真或假):
\groupAtrue % 或者
\groupAfalse
即:
\<conditionalsname>true
\<conditionalsname>false
取决于你希望为条件设置的值。
-
现在可以在
if
控制结构中使用条件语句。
\ifgroupA
% 在这里写给 A 组的文档代码
\else
% 在这里写给其他人群体的文档代码
\fi
完整示例:
\newif\ifdirector
% 设置条件为假
\directorfalse
\ifdirector
I write something for the director.
\else
I write something for common people.
\fi
输出:
I write something for common people.
案例语句
语法如下:
\ifcase <number><case0>\or<case1>\or...\else<defaultcase>\fi
如果数字等于某个案例编号,则打印该内容。注意,编号从 0 开始。
\ifcase 2 a\or b\or c\or d\else e\fi
输出:
c
\else
用于指定默认情况(当没有其他案例匹配时)。
循环
基础语法如下:
\loop <content> \if*<condition><true action>\repeat
和以往一样,content
和 true action
是任意的 TeX 内容。\if*
指代任何条件语句。注意没有“假”动作,你不能在 \if*
和 \repeat
之间使用 \else
。如果需要,可以通过改变条件或使用 \newif
定义新的条件语句来解决。示例:
\count255 = 1
\loop
\TeX
\ifnum\count255 < 10
\advance\count255 by 1
\repeat
这段代码将打印 TeX
十次。
什么都不做
有时你可能需要告诉 TeX 你什么都不做。可以使用两个命令:\relax
和 \empty
。
经典示例:
\def\myspace{\hskip 25pt\relax}
\myspace{} plus 10pt
\relax
防止在命令后遇到加号或减号时产生不期望的行为。
\empty
与 \relax
的区别在于扩展:\empty
在宏扩展后会消失。
TeX 字符
我们可以使用 \char {charcode}
命令打印所有字符。charcode
实际上是字节值。例如:
\char65 = \char `A = \char `\A
大多数字符对应于 ASCII 值(例如 A-Z、a-z),一些字符替代了 ASCII 中的不可打印字符。
chardef 和 mathchardef
你可以定义控制序列,使其展开为特定的字符。语法为 \chardef<控制序列>=<字符代码>
。以下序列做了相同的事情:
\chardef\myA=65
\chardef\myA=`A
\chardef\myA=`\A
示例:
\mathchardef\alphachar = "010B
$\alphachar$
\count255 = 0
\loop
[\number\count255 =\char\number\count255]
\ifnum\count255 < 127
\advance\count255 by 1
\repeat
另一种版本,使用不同字体,每行一个条目:
\count255 = 0
\loop
[\number\count255 =
\char\number\count255 \
{\tt \char\number\count255}
{\it \char\number\count255}
]
\hfil\break
\ifnum\count255 < 127
\advance\count255 by 1
\repeat
Verbatim 行和空格
发现(La)TeX 将所有空格视为相同类型的间距胶水可能会让人困惑。Plain TeX 提供了一些命令来保留你写入的空格和换行符:
\begingroup
\obeylines
\obeyspaces
Relevant text here
\endgroup
这意味着你可能需要将自己的 verbatim 环境和命令组合起来:
\newenvironment{myverbatim}{\begingroup \obeylines \obeyspaces}{\endgroup}
\newcommand{\mycommand}[n]{do something with #1 .. #n}
然后在你的 .tex
文件中:
\begin{myverbatim}
\mycommand{
whichever text it is important you
preserve the spacing and newslines
for, like when you want to generate
a verbatim block later on.
}
\end{myverbatim}
宏定义宏
在某些情况下,这非常有用,例如为了定义语言命令,如在《多语言版本》中所解释的那样,用户可以写:
\en{some english text}
\de{etwas deutscher Text}
并确保它切换到适当的 Babel 语言。
让我们定义一个宏,用来定义语言命令。这些命令很简单:如果参数是 \locale
变量的值,那么相应的宏直接打印它的内容。否则,它什么也不做。
基本上,我们要做的是定义一组这样的宏:
\newcommand{\de}[1]{#1}
\newcommand{\en}[1]{}
\newcommand{\fr}[1]{}
在上述代码片段中,只有 \de
命令会输出它的内容, \en
和 \fr
不会输出任何内容。这正是我们想要的。问题出现在你想自动化任务,或者有很多语言时,你想更改语言选择。你只需要移动 #1
,但这并不方便,也无法从命令行选择 Babel 语言。思考一下...
我们将动态定义语言命令,基于 \locale
变量(或任何你选择的变量)。因此,使用 ifthen
包中的 \equal
命令。
由于在 LaTeX 中编写是非常困难的,我们将使用一些 Plain TeX。
\def\locale{de}
\def\localedef#1{
\ifthenelse{ \equal{\locale}{#1} }{
%% 设置 Babel 语言。
%% 定义命令来打印内容。
}{
%% 定义命令打印空内容。
}
}
另一个问题是:如何定义一个名称是变量的命令?在大多数编程语言中这是不可能的。我们可能会想要写:
\def\#1 #1{#1}
这会失败有两个原因:
-
最后的两个
#1
应该指向新宏的参数,但它们会先展开为\localedef
宏的第一个参数,因为它们在该宏的正文中。 -
\#1
会展开成两个符号:#
和1
,而\def
命令会失败,因为它需要一个有效的控制序列名。
问题1的解决方案很简单:使用 ##1
,它在宏执行时会展开为 #1
。
对于问题2,解决方法有点棘手。我们可以告诉 TeX 特定的符号是一个控制序列。\csname...\endcsname
就是用来做到这一点的。然而:
\def\csname#1\endcsname ##1{##1}
会失败,因为它会重新定义 \csname
为 #1
,这不是我们想要的,然后 TeX 会遇到 \endcsname
,从而导致错误。
我们需要延迟 \def
的展开,即告诉 TeX 先展开 \csname
部分,然后应用 \def
。有一个命令可以做到这一点:\expandafter{token1}{token2}
。它会先展开 {token2}
,然后再展开 {token1}
。
最终,如果我们想从命令行设置语言,我们必须能够设置 \locale
变量,使得源代码中的语言成为默认值,可以被命令行中的值覆盖。这可以通过 \providecommand
来实现:
\providecommand\locale{fr}
最终的代码是:
%% 必需的包
\usepackage{ifthen}
%% 生成语言命令的 TeX 函数
\def\localedef#1#2{
\ifthenelse{ \equal{\locale}{#1} }{
\selectlanguage{#2}
\expandafter\def\csname#1\endcsname ##1{##1}
}{
\expandafter\def\csname#1\endcsname ##1{}
}
}
%% 选择的语言,可以放在语言命令之前的任何位置
\providecommand\locale{fr}
%% 语言命令
\localedef{de}{ngerman}
\localedef{en}{english}
\localedef{fr}{frenchb}
%% ...
你可以使用以下命令进行编译:
latex '\providecommand\locale{en}\input{mydocument.tex}'
附录
参考文献和更多阅读:
-
《The TeXbook》, Donald Knuth
-
《TeX by Topic》, Victor Eijkhout
-
《TeX for the Impatient》, Paul W. Abrahams, Karl Berry 和 Kathryn A. Hargreaves
-
《TeX command reference》在维基书