网球比分直播第一 www.gqgyzr.com.cn

架構 / 層次設計

上次我們已經介紹過,LaTeX3 的主要成果都凝結在了 expl3 之中。實際上,LaTeX3 是一套非常龐大的框架,集編程和排版為一體。根據我們之前的介紹,LaTeX3 為此做出了一個層次劃分:

  • 文本標記層。這一層主要提供給文章作者使用。顯然,考慮到歷史兼容性,這一層次與我們至今仍在使用的 LaTeX 2ε 并沒有顯著的差異。對于 LaTeX 的一般用戶而言,從 LaTeX?2ε 轉換到 LaTeX3 的學習成本可以說幾乎為零;甚至由于一些新接口、新語法的使用,使用 LaTeX 將變得更加方便。

  • 設計接口。傳統的 LaTeX?2ε 并沒有提供這一個層次。也就是說,用戶要么使用 LaTeX 本身或者宏包提供的功能,要么就必須通過底層編程來進行控制,不存在這樣一個所謂「設計模板」的存在。編寫模板的人,我們暫且可以稱之為是「設計師」,他們只需要利用編程框架設計模板,而無需考慮用戶(即文章作者)究竟用模板寫了怎樣的內容。

  • 編程接口。這一層次實際上就是 expl3,它的實現基于 TeX 的原語,提供了豐富的編程工具,也是上面兩個層次的實現手段。

這三個層次是緊密聯系在一起的。如前所述,expl3 宏包提供了編程接口;xtemplate 宏包給出了實現「文檔原型」的方法,也就是提供了上面所說的「設計接口」;最后,xparse 宏包用來定義文檔層的命令和環境,即所謂「文本標記」。

LaTeX3 相關宏包

對于具體的用戶來說,無論是文章作者、設計師還是程序員,使用 LaTeX3 在目前階段仍需要通過調用一系列宏包來完成。

目前,在 CTAN 中與 LaTeX3 相關的有五個軟件包[1]

  • l3kernel:包含了 expl3 宏包的各個部分。

  • l3packages:提供較高層次的接口(設計層和文本標記層),這些宏包的語法接口都較為穩定。主要包括:

    • l3keys2e

    • xfp

    • xfrac

    • xparse

    • xtemplate

  • l3experimental:一些實驗性的嘗試,同樣用來構建較高層次的語法接口,但不如l3packages穩定。目前主要有:

    • l3bench-mark

    • l3c-ctab

    • l3-color

    • l3-draw

    • l3str

    • l3sys-shell

    • xcoffins

    • xgal-ley

  • l3backend:提供與后端(底層驅動)相交互的代碼,處理顏色、繪圖、PDF 特性等功能,目前主要支持以下幾種驅動:[2]

    • dvipdfmx

    • dvips

    • dvisvgm

    • xdvipdfmx

    • PDF 模式(即 pdf-TeX 和 Lua-TeX)

  • `l3build`:LaTeX3 的構建系統,用來進行單元測試、文檔排版、自動化發布等。它利用一系列 Lua 腳本來實現跨平臺的功能。

這就是當前 LaTeX3 的主要組成。除此以外,在 LaTeX3 的 GitHub 存儲庫中,忽略文檔、測試文件和輔助文件等,還有以下幾個部分:

  • l3trial

  • l3leftovers

  • xpackages

這是一些高度實驗性的功能以及一些棄用的???。對于普通用戶和開發者來說,它們不應該直接使用。

命名規范

前面鋪墊了很多,現在我們終于可以開始嘗試 LaTeX3 了。

和我們熟知的 LaTeX?2ε 不同,LaTeX3 對函數變量做出了區分。函數可以吃掉一些參數,并進行相應的操作;函數要么是可被展開的,要么就是可被執行的,以后講到展開控制的時候我們還會詳細介紹。變量用來存儲數據,它會被函數所調用。一些具有相關功能的函數和命令可以構成一個???/strong>。

LaTeX3 中的命令,無論是函數還是變量,仍然都是以反斜杠 \ 開頭。所不同的是,我們可以在命令中使用下劃線 _,用以區分不同單詞。

函數

按照規范,LaTeX3 中的函數名包括三部分:??槊?code style="font-size: inherit; line-height: inherit; overflow-wrap: break-word; padding: 2px 4px; border-radius: 4px; margin: 0px 2px; color: rgb(233, 105, 0); background: rgb(248, 248, 248);">module)、描述(description)以及參數指定(arg-spec),形如

\<module>_<description>:<arg-spec>

注意參數指定需要放在冒號 : 后面。不必奇怪,冒號也是命令的一部分。

參數指定

??槊朊枋齙暮宥際竅遠準??!覆問付ā?,指的是這一函數要吃掉怎樣的一些參數,它由一串字母組成(區分大小寫)。最基本的參數指定包括:

  • n:普通(normal)參數,表示一組由大括號 {…} 包圍的 token(記號,或者叫字元)列表,這其實就是TeX 中的標準宏參數

  • N:表示單個 token,比如一個控制序列(由 \ 開頭的命令),或者一個單獨的字符

  • p:原始 TeX 的形參(parameter)指定。具體來說,就是我們在用 \def 定義新命令時所用的 #1、#1#2

  • T, F:這兩個是 n 的特殊情況,用來給出條件分支(True、False)

還有兩個特殊的參數指定:

  • D:表示不要使用(Do not use)。由 D 開頭的命令是原語的封裝,在 l3kernel 之外盡量不要直接使用(當然有時候不可避免)

  • w:奇異型(weird)參數,表示不遵循標準參數指定的一些特例

參數指定在 LaTeX3 中發揮著至關重要的作用。LaTeX3 的展開控制機制將會引入更多類型的參數指定,以后我們會詳細介紹。

函數的例子

  • \cs_new:Npn:這一函數屬于cs??椋刂菩蛄?,control sequence)。顧名思義,它用來創建新的函數。三個參數分別是:

    這一函數的行為類似于 \def:[3]

    % LaTeX2ε
    \def\myfunc#1{Hello #1}
    % LaTeX3
    \cs_new:Npn \my_func:#1 { Hello~ #1 }

    注意到開啟 LaTeX3 語法后,單詞間的空格是不起任何作用的(catcode=9,即可忽略字符)。確實要使用空格時,則用 ~ 代替。至于需要使用 ~ 的原來意思,即不可斷開的空格(俗稱「帶子」)時,可以用原來的宏 \nobreakspace。

    • N:函數名稱,由于是 \ 開頭的控制序列,因而總是單個 token

    • p:新創建的函數的形參指定

    • n:具體的函數定義

  • \int_if_even:nTF:它屬于 int ???,用于處理整數。這一函數的作用是判斷一個數字(由 n參數接受)是不是偶數,若是,則執行 T 分支,否則執行 F 分支。

    \int_if_even:nTF { 12 }
    { <true code>  }
    { <false code> }

    顯然以上這段代碼會執行 <true code>。

    一般來說,這種條件判斷函數在定義時會同時創建多種分支結構,比如 \int_if_even:nT\int_if_even:nF 也可以使用。\int_if_even:nT 表示數字為偶數則執行 T 分支,否則什么也不做;\int_if_even:nF 也是類似的。

變量

LaTeX3 中,變量的名稱包括四個部分:作用域(scope)、??槊?code style="font-size: inherit; line-height: inherit; overflow-wrap: break-word; padding: 2px 4px; border-radius: 4px; margin: 0px 2px; color: rgb(233, 105, 0); background: rgb(248, 248, 248);">module)、描述(description)以及變量類型(type),形如

\<scope>_<module>_<description>_<type>

通常來說,變量名中只包含字母和下劃線(_)。

作用域

變量的作用域有三種:

  • c:表示常數(constant),即一旦創建,就不應該改變它的值

  • g:全局變量(global),它的值是全局有效的,也就是分組({…})對其無效。常見的例子比如某些計數器變量

  • l:局部變量(local),顧名思義,它們的值只在局部有效。在大多數情況下,我們使用的都是 l型變量

全局變量和局部變量的修改需要通過不同的函數來進行。比如把一個 int 變量設為零,我們有兩個函數:\int_zero:N\int_gzero:N。前者是局部有效的,應當作用于 l 型變量;而后者是全局有效的,作用于 g 型變量。

具體到應用,我們來看一個例子:

% 聲明變量
\int_new:N \l_my_variable_int
\int_new:N \g_my_variable_int
% 查看變量的值
\int_show:N \l_my_variable_int % => 0
\int_show:N \g_my_variable_int % => 0
% 開啟一個分組
{
  % 賦值
  \int_set:Nn  \l_my_variable_int { 1 }
  \int_gset:Nn \g_my_variable_int { 1 }
  % 查看變量的值
  \int_show:N \l_my_variable_int % => 1
  \int_show:N \g_my_variable_int % => 1
}
\int_show:N \l_my_variable_int % => 0
\int_show:N \g_my_variable_int % => 1

\int_new:N 表示全局地創建一個 int 型變量,并且賦初值為 0。在分組中,我們分別把 l 型和 g 型變量的值修改為 1;但離開分組,可以看到 l 型變量的值仍為 0,而 g 型變量的值則同分組中的一樣,被修改為了 1。

需要指出的是,TeX 實際上并不會在乎究竟把變量的名字起做什么,所以這種作用域的劃分,更多的是一種慣例(convention)或者風格(style),而非硬性的語法規定。不過出于可讀性的考慮,一般情況下仍然要求遵循這一規范。

變量類型

我們知道,C 語言里面有 int、double、char 這樣的數據類型。TeX 中也有類似的概念,稱為寄存器(register)。寄存器共有 6 種:

  • count:計數器,相當于整型變量

  • toks:記號變量(tokens

  • box:盒子變量

  • dimen:剛性長度(dimension)

  • skip:彈性長度

  • muskip:數學彈性長度(skip in math unit)

LaTeX3 在 TeX 的基礎上做了很大的擴充,新定義了一些新的變量類型。

以下這幾種直接繼承了前面所說的寄存器類型:

  • box

  • int <– count (integer)

  • dim <– dimen

  • skip

  • muskip

以下是 LaTeX3 新定義的變量類型,它們大多只是一些特殊的宏:

  • 數據結構:

    • `tl`:記號列表(token list)

    • `str`:字符串(string),它與 `tl` 的區別在于忽略了類別碼(除空格外全部設為其他類 12,空格仍為 10)

    • `seq`:序列(sequence),棧

    • `clist`:逗號分隔列表(comma list

    • `prop`:屬性列表(property list),即關聯列表

    • `fp`:浮點數(floating points)

    • `intarray`、`fparray`:整型、浮點型數組(integer/floating point array

  • 盒子的推廣:

    • `coffin`:帶「把手」的盒子

  • 其他:

    • `bool`:布爾型變量

    • `token`:記號

    • `ior`、`iow`:輸入、輸出流(I/O read/write)

    • `regex`:正則表達式(regular expression)

還有幾種比較特殊的變量,它們不遵循通常的命名規則:

  • quark:「夸克」,是展開到自身的宏

  • mark:掃描標記

在某些地方,比如 LaTeX3 的內部實現中,這兩種變量會發揮重要的作用。

變量的例子

  • \c_pi_fp:常數圓周率

  • \l_tmpa_tl、\g_tmpa_tl:臨時 token list 變量,注意這里做了局部與全局的區分

  • \q_stop:這是一個「夸克」,常用來作為某些參數列表的分界符

以上這幾個變量屬于 l3kernel 的編程接口,所以沒有指定??槊?。

LaTeX3 中的變量與相關函數組成了一個個???。之后我們就將分??櫓鷚喚檣?LaTeX3 的各種功能。

私有函數與變量

按照 LaTeX3 的規范,所有的公開函數及變量都需要給出注釋或說明。除此之外,在編程的時候或多或少會引入一些私有的函數與變量,而我們并不希望普通用戶以及其他宏包的作者使用它們。

私有函數以兩個下劃線開頭,如 \__my_function:nn;私有變量則在作用域標記之后跟著兩個下劃線,如 \l__my_variable_tl。

事實上,LaTeX3 提供了 l3docstrip 宏包,它在文學編程宏包 docstrip 的基礎上引入了名字空間的手法。下面我們來給出一個例子:

% 進入名字空間 `myi`
%<@@=myi>
\cs_new:Npn \myi_function:#1
  { \@@_function:nn {#1} { \@@_foo_int } }
\int_new:N \@@_foo_int
\cs_new:Npn \@@_function:nn #1#2
  { ... }
% 進入名字空間 `myii`
%<@@=myii>
\cs_new:Npn \myii_function:#1
  { \@@_function:nn {#1} { \myi_function:n {#1} } }
\cs_new:Npn \@@_function:nn #1#2
  { ... }
% 關閉名字空間
%<@@=>

此處,我們看似定義了兩個 \@@_function:nn 函數。但實際上,它們分別是 \__myi_function:nn\__myii_function:nn,所以并不會發生沖突。

更重要的是,在???myii 中,我們不能用 @@ 的簡寫形式來調用 myi 中的私有成員,而應該盡量使用 myi ??樘峁┑墓涌冢?\myi_function:n)。因此,即使某一宏包(??椋┑哪誆糠⑸吮浠?,只要接口不變,使用它的其他??榫筒換岣惺艿秸庵直浠?。這正是封裝的作用。

直接調用其他??櫓械乃接諧稍保ū熱?\__myi_function:nn)也并非不可以,有的時候還必須如此。不過一旦原來??櫸⑸吮潿?,調用的地方也需要相應做出改變。

代碼風格

我們知道,Google 為 C++ 和 Python 等提供了格式指南(style guide),但傳統上 TeX 和 LaTeX 這樣的宏語言卻并沒有類似的代碼規范,因此很多時候可讀性實在不敢恭維。

通過修改空格、下劃線等字符的類別碼,LaTeX3 大大提供的代碼的可讀性。這樣,我們也可以相應地給出一些格式規范:

  • 每行不超過 80 個字符

  • 各元素之間添加空格以增加可讀性,除了少數使用簡單參數的情況,如 {#1}、#1#2

  • 每一層語義應當獨占一行,比如 true 和 false 分支就應至少占據兩行

  • 對不同層次的代碼合理使用縮進。縮進可以使用兩個空格,但不要用 tab

  • 左花括號單獨占據一行,并且也需要縮進

以下是一個示例:

\cs_new:Npn \my_foo:nn #1#2
  {
    \tl_if_empty:nTF {#1}
      { \my_foo_aux:n { X #2 } }
      {
        \my_foo_aux:nn {#1} {#2}
        \my_foo_aux:n { #1 #2 }
      }
  }

當然,沒有一種規范是可以放之四海而皆準,特殊的地方總還是免不了特殊對待。

編程環境

之前我們就已經提到過,LaTeX3 目前為止還沒有成為一個獨立的格式。使用 LaTeX3,仍然需要在 LaTeX?2ε 中調用 expl3 宏包。

如果只在一個 .tex 文件中使用,可以這樣做:

\documentclass{article}
\usepackage{expl3}

\ExplSyntaxOn   % 開啟 LaTeX3 編程環境
...
\ExplSyntaxOff  % 關閉 LaTeX3 編程環境

如果是要編寫宏包或文檔類,標準做法與在 LaTeX?2ε 中類似:

\RequirePackage{expl3}

% 宏包使用 \ProvidesExplPackage
% 文檔類使用 \ProvidesExplClass
% 其他文件使用 \ProvidesExplFile
\ProvidesExplPackage{<package>}{<data>}{<version>}{<description>}

% 之后開啟 LaTeX3 語法,文件末尾處則會自動關閉

第二種方法繼承并擴展了 LaTeX?2ε\ProvidesPackage、\ProvidesClass\ProvidesFile 的功能,大致相當于

% Package info

%
 文件開頭
\makeatletter
\ExplSyntaxOn
...
% 文件結尾
\ExplSyntaxOff
\makeatother

因此在編寫宏包或文檔類時,@ 符號可以被當成字母使用。

注釋

  1. [^]這里的「軟件包」是指一系列宏包、文檔等的集合,可以通過 tlmgr 一類的包管理器進行安裝、更新、備份等操作。注意與 LaTeX 語境下的「宏包」相區分,它是后綴名為 .sty 的 TeX 文件,通過 \usepackage 調用。

  2. [^]2019 年 7 月 l3backend 的代碼從 l3kernel 中獨立出來,以便采取不同的更新策略。

  3. [^]與 \def 不同的是,\cs_new:Npn 會做重復定義檢查,如果命令已經定義則會報錯;此外還加上了 \long,即允許在參數中使用 \par。因而 \cs_new:Npn 的實際效果其實更接近 LaTeX?2ε 中的 \newcommand,只是參數形式更加靈活(\newcommand 只能定義不帶參數,或者參數形如 [<可選參數>]{<必選參數 1>}… 的命令)。

參考

點擊閱讀下一篇:LaTeX3教程(三)—— 從一個例子說起


選自:https://stone-zeng.github.io/2019-02-26-l3tutorial-syntax/#fn:l3backend