LaTeX/Plain TeX
當你使用 LaTeX 宏時,你會發現它非常有限。你可能會好奇,你每天使用的所有這些軟體包是如何用這麼少的程式碼實現的。事實上,LaTeX 是一套 Plain TeX 宏,大多數軟體包都使用 Plain TeX 程式碼。Plain TeX 的級別要低得多,它有更多功能,但學習曲線陡峭,程式設計複雜。
除了少數例外,你可以在有效的 LaTeX 文件中使用完整的 Plain TeX 語言,反之則不然。
詞彙
[edit | edit source]為了避免混淆,有必要解釋一些術語。
- 一個 組 是在開括號之後和匹配的閉括號之前的任何內容。
- 一個 標記 是一個字元、一個控制序列或一個組。
- 一個 控制序列 是任何以
\開頭的內容。它不會按原樣列印,而是根據其型別由 TeX 引擎進行擴充套件。 - 一個 命令 (或 函式 或 宏)是一個控制序列,它可能擴充套件為文字,控制序列的(重新)定義等。
- 一個 原語 是一個在 TeX 引擎中硬編碼的命令,即 它不是用 Plain TeX 編寫的。
- 一個 暫存器 是 TeX 處理變數的方式。它們的數量是有限的(在經典 TeX 中,每種型別的暫存器有 256 個,在 e-TeX 中有 32767 個)。
- 一個 長度 是一個包含長度的控制序列(一個數字後跟一個單位)。參見 長度。
- 一個 字型 是一個引用字型檔案的控制序列。參見 字型。
- 一個 盒子 是一個用於列印的物件。出現在紙張上的任何內容都是一個盒子:字母、段落、頁面......參見 盒子。
- 一個 膠水 是一個特定的空間量,當盒子被連線在一起時,它被放置在盒子之間。
- 一個 計數器 是一個包含數字的暫存器。參見 計數器。
可能還有更多的術語,但我們希望現在已經足夠了。
類別碼
[edit | edit source]在 TeX 中,一些字元具有特殊的含義,而不是列印相關的字形。例如,\ 用於引入控制序列,預設情況下不會列印反斜槓。
為了區分字元的不同含義,TeX 將它們分成 類別碼,簡稱 類別碼。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 刪除) |
活動字元
[edit | edit source]活動字元類似於宏:它們是單個字元,將在任何其他命令之前進行擴充套件。
\catcode`| = 13
\def|{\TeX}
...
This is a stupid example of |.
|
這是一個關於 TeX 的愚蠢示例。 |
請注意,活動字元需要直接後跟定義,否則編譯將失敗。
示例
[edit | edit source]- Texinfo
Texinfo 使用類似於 TeX 的語法,但有一個主要區別:所有函式都以 @ 而不是 \ 開頭。這並非偶然:它實際上使用 TeX 列印檔案的 PDF 版本。它基本做的就是輸入texinfo.tex它重新定義了控制序列字元。可能的實現
\catcode`\@=0
@def@@{@char64} % To write '@' character.
\catcode`\\=13 @def\{{@tt @char92}}
The @TeX command was previously written '\TeX'. It is now written '@@TeX'.
|
TeX 命令以前寫成 '\TeX'。現在寫成 '@TeX'。 |
透過這種重新定義,'@' 現在應該引入每個命令,而 '\' 實際上將列印一個反斜槓字元。
- 專案符號
有些人可能發現 LaTeX 列表環境的語法有點繁瑣。這裡有一個快速定義類似維基的專案符號的方法
\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
[edit | edit source]如果您進行了一些 LaTeX 程式設計,您一定遇到過這兩個命令,\makeatletter 和 \makeatother。
在 TeX 中,'@' 字元預設屬於類別碼 11 字母。這意味著您可以將其用於宏名稱。LaTeX 利用類別碼來指定規則:所有非公共的、內部的宏,其名稱中至少包含一個 '@' 字元,這些宏不應由終端使用者訪問。在文件中,LaTeX 將 '@' 的類別碼更改為 12,即 其他。
這就是為什麼當您需要訪問 LaTeX 內部函式時,必須將所有訪問私有函式的命令括在 \makeatletter 和 \makeatother 之間。它們所做的只是更改類別碼
\def\makeatletter{\catcode`@ = 11}
\def\makeatother{\catcode`@ = 12}
|
普通 TeX 宏
[edit | edit source]\newcommand 和 \renewcommand 是 LaTeX 特定的控制序列。它們檢查沒有現有的命令被新定義所覆蓋。
在普通 TeX 中,用於宏定義的原語不會對可能的覆蓋進行檢查。您需要確保沒有破壞任何東西。
語法是
\def<macroname>#1<sep1>#2<sep2>{macro content, use of argument #1, blah, #2 ...}
|
您可以在引數之間使用(幾乎)任何字元序列。例如,讓我們編寫一個簡單的宏,它將小數點分隔符從點更改為逗號。首先嚐試
\def\pointtocomma #1.#2{(#1,#2)}
%%...
\pointtocomma 123.456
|
這將列印 (123,4)56。我們添加了括號只是為了突出顯示這裡的問題。每個引數是最短的可能的輸入序列,與宏定義匹配,包括分隔符。因此 #1 匹配直到第一個點的所有字元,而 #2 僅匹配第一個標記,即 第一個字元,因為它之後沒有分隔符。
解決方案:新增第二個分隔符。空格可能看起來很方便
\def\pointtocomma #1.#2 {(#1,#2)}
|
一般來說,每當您希望使用特定分隔符獲得多個引數時,都要考慮最後一個分隔符。如果您不想使用分隔符,那麼普通 TeX 宏的使用方式與 LaTeX 宏相同(沒有預設引數)
\def\mymacro#1#2#3{{\bf #1}#2{\bf #3}}
%% ...
\mymacro{word1}{word2 word3}{!!!}
|
擴充套件定義
[edit | edit source]TeX 還有另一個定義命令:\edef,它代表 擴充套件定義。語法保持不變
\edef<macroname><argumentslist>{<expanded content>}
|
內容在使用 \edef 的地方被擴充套件(但不會執行,即 列印),而不是在定義的宏被使用的地方。宏擴充套件並不總是顯而易見的...
示例
\def\intro{Example}
\edef\example#1{\intro~---~#1}
\def\intro{Exercise}
\example{This is an example}
|
這裡 \intro 的重新定義對 \example 不會有任何影響。
全域性定義
[edit | edit source]定義僅限於其範圍。但是,有時將宏定義在一個組中,使其在該組之外以及直到文件結束時仍然有效,這可能很方便。這就是我們所說的 全域性定義。
{
\def\LocalTeX{Local\TeX}
\global\def\GlobalTeX{Global\TeX}
}
I can still access the \GlobalTeX{} macro here.
|
您也可以將 \global 命令與 \gdef 結合使用。
這兩個命令都有快捷方式
\gdef用於\global\def\xdef用於\global\edef
長定義
[edit | edit source]之前的定義命令不允許您在多個段落中使用它們,即 包含 \par 命令(或雙行換行符)的文字。
您可以在定義之前加上 \long 命令,以允許使用多段落引數。
示例
\long\def\dummy#1{#1}
\dummy{First paragraph\par Second paragraph}
|
外部定義
[edit | edit source]此字首宏阻止定義在某些上下文中使用。它有助於合併宏並使其因錯誤的上下文而更不容易出錯。外部宏 旨在在任何上下文之外使用,因此得名。
例如,以下程式碼將失敗
\outer\def\test{a test}
\def\failure{\test}
|
外部宏不允許出現在
- 宏引數
- 跳過的條件
- ...
let 和 futurelet
[edit | edit source]\let<csname><token> 與 \expandafter\def\expandafter<csname>\expandafter{<content>} 相同。它定義了一個新的控制序列名稱,該名稱等效於指定的 token。該 token 通常是另一個控制序列。
請注意,\let 只會擴充套件 token 一次,這與 \edef 相反,\edef 將遞迴擴充套件,直到不再可能進一步擴充套件。
示例[1]
Using let:\par
\def\txt{a}
\def\foo{\txt}
\let\bar\foo
\bar % Prints a
\def\txt{b}
\bar % Prints b
Using edef:\par
\def\txt{a}
\def\foo{\txt}
\edef\bar{\foo}
\bar % Prints a
\def\txt{b}
\bar % Prints a
|
\futurelet<csname><token1><token2>... 的工作方式略有不同。首先,token2 被分配給 csname,然後 TeX 處理 <token1><token2>... 序列。因此,\futurelet 允許您在使用標記後立即分配它。
特殊的控制序列名稱
[edit | edit source]某些宏的名稱可能無法直接寫入。對於由宏名稱組成的宏名稱,情況就是如此。示例
\def\status{full}
\def\varempty{This is empty}
\def\varfull{This is full}
\csname var\status \endcsname
|
最後一行將根據 \status 列印一個句子。
此命令實際上與 \string 相反,\string 會列印一個控制序列名稱,而不會擴充套件它
{\tt \string\TeX}
|
\TeX |
控制擴充套件
[edit | edit source]\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 型別的內容
- 類別碼
- 字元定義
- 字型引數
- 內部引數
- 長度
- 暫存器
- ...
示例
Text dimensions: $ \the\hsize \times \the\vsize $
|
暫存器是一種型別的變數。它們的數量有限,從 0 到 255。共有 6 種不同的型別
| 型別 | 描述 |
|---|---|
| 盒子 | 一個盒子 |
| 計數器 | 一個整數 |
| 尺寸 | 一個長度 |
| 粘性 (mu 單位) | 一個粘性 (mu 單位) |
| 粘性 | 一個粘性 |
| 令牌 | 一個令牌序列 |
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 可以是計數器、尺寸、粘性 (mu 單位) 或粘性型別。它對盒子和令牌沒有意義。
基本語法是
\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
|
這是假的 |
你可以使用 \newif 命令建立新的條件語句(作為一種 布林變數)。透過這些自定義條件語句,你可以以一種優雅的方式控制程式碼的輸出。說明條件語句用法的最佳方法是透過一個例子。
必須生成兩個版本的文件。一個版本是針對 A 組的,另一個版本是針對其他所有人的(即不屬於 A 組的)。
1. 我們使用 \newif 來定義我們的條件語句(即布林變數)。
\newif\ifgroupA
|
2. 以下方式為我們的條件語句設定一個值(真或假)
\groupAtrue % or
\groupAfalse
|
也就是說
\<conditionalsname>true
\<conditionalsname>false
|
取決於我們希望在條件語句中設定哪個值。
3. 現在我們可以在之後的任何地方使用我們的條件語句,在 if 控制結構 中。
\ifgroupA
% Here we write the code of the document that is
% intended for the group A
\else
% Here we write the code of the document that is
% intended for the rest of the people
\fi
|
一個完整的例子是
\newif\ifdirector
%I set the conditional to false
\directorfalse
\ifdirector
I write something for the director.
\else
I write something for common people.
\fi
|
我寫一些針對普通人的東西。 |
語法是 \ifcase <number><case0>\or<case1>\or...\else<defaultcase>\fi。如果 number 等於情況編號,它的內容將被列印。注意,它從 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* 指的是任何 條件語句。注意,沒有 false action,你不能在 \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
|
如果在命令之後遇到 plus 或 minus,\relax 將阻止出現不希望的行為。
\empty 和 \relax 之間的區別在於展開:\empty 在宏展開後會消失。
我們可以使用 \char {charcode} 命令列印所有字元。charcode 實際上是位元組值。例如
\char65 = \char `A = \char `\A
|
大多數字符對應於 ASCII 值(例如 A-Za-z),一些字元替換了 ASCII 中的不可列印字元。
你可以定義控制序列以展開為特定字元。語法是 \chardef<control sequence>=<charcode>。以下序列執行相同的操作。
\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
|
發現 (La)TeX 將所有空白都視為同一型別的間距粘性,這一點令人困惑。Plain TeX 提供了一些命令來保留你寫的間距和換行符
\begingroup
\obeylines
\obeyspaces
Relevant text here
\endgroup
|
這意味著你可能需要組合自己的逐字文字環境和你的命令
\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} }{
%% Set the Babel language.
%% Define the command to print the content.
}{
%% Define the command to print nothing.
}
}
|
另一個問題出現了:如何定義一個名稱是變數的命令?在大多數程式語言中,這根本不可能。我們可以嘗試寫的是
\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}。它將在{token1}之前擴充套件{token2}。
最後,如果我們想從命令列設定語言,我們必須能夠設定\locale變數,以便原始碼中的變數是預設值,可以被命令列中的變數覆蓋。這可以透過\providecommand來實現
\providecommand\locale{fr}
|
最終程式碼是
%% Required package.
\usepackage{ifthen}
%% TeX function that generates the language commands.
\def\localedef#1#2{
\ifthenelse{ \equal{\locale}{#1} }{
\selectlanguage{#2}
\expandafter\def\csname#1\endcsname ##1{##1}
}{
\expandafter\def\csname#1\endcsname ##1{}
}
}
%% Selected language. Can be placed anywhere before the language commands.
\providecommand\locale{fr}
%% Language commands.
\localedef{de}{ngerman}
\localedef{en}{english}
\localedef{fr}{frenchb}
%% ...
|
您可以使用以下命令進行編譯
latex '\providecommand\locale{en}\input{mydocument.tex}'
- ↑ 來自 tex.stackexchange.com: \let 和 \edef 之間的區別是什麼?
- 進一步閱讀
- TeXbook,Donald Knuth
- TeX by Topic,Victor Eijkhout
- TeX for the Impatient,Paul W. Abrahams, Karl Berry 和 Kathryn A. Hargreaves
- TeX 命令參考在 wikibooks 中
