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

確定目標

學習一門編程語言,如果只了解語法,必然十分枯燥而且并沒有什么用。所以,我們準備從一個基本的例子講起,盡可能地覆蓋 LaTeX3 的重要知識。

這里的例子,是一個中文測試文字(亂數假文)宏包。假文的目的是生成大段沒有實際含義的文字,常用來測試排版效果。對于西文,TEX 發行版中已經自帶了幾個宏包,包括 lipsum、kantlipsumblindtext 等;而對于中文,則由我本人編寫了 zhlipsum。我們的目標,就是在這幾篇教程中,讓大家完成一個類似 zhlipsum 的宏包??垂鹺Q蟆禠aTeX 入門》一書的讀者就會發現,這實際上正是書中的練習 8.6。

要編寫中文測試文字宏包,首先要做一下整體規劃與設計:

  • 最簡單的實現,只要定義一些含有大量文字的命令,并且能使用戶方便使用。

  • 接下來,為了能夠改變假文片段的長度,我們就需要用到計數、循環等功能,同時還會引入一些基本的數據結構。

  • 之后,因為面向的是中文測試,我們的宏包還要能夠支持多種字符集和編碼,這需要通過類別碼機制來處理。

  • 最后,作為一個完整的宏包,還需有一套良好的接口和完善的錯誤提示,以方便用戶使用。

當然,如果可能,我們還會考慮介紹一些高級功能,比如:

  • 利用偽隨機數生成真正的「亂數」假文。

  • 通過 DocStripdoc 宏包進行文學編程。

  • 構建覆蓋面足夠的測試,完成宏包發布。

準備

開發環境配置

考慮到 LaTeX3 仍處于活躍開發狀態,建議使用最新的 TEX 發行版。另一方面,開發 LaTeX 宏包與日常寫作文檔稍有區別,很多時候我們需要通過命令行輸出進行調試,而不僅僅是編譯 TEX 文檔以生成 PDF。所以,建議使用命令行直接編譯。為使用 TEX 寫作開發的編輯器,如 WinEdt、TeXstudio 等,反而可能不太適用于此。我們的項目會涉及到漢字處理,因此要保證編輯器對多種編碼均有良好支持。

我本人使用的是 TeX Live 2019 + Visual Studio Code。

此外,如果要開始一個正式的項目 / 工程,強烈建議建立一個 Git 倉庫來管理。具體做法可參閱廖雪峰的 Git 教程。

目錄結構

我們打算把這個宏包取名為 zhdummy。名字當然可以任意取,但確定之前請務必在網上檢索一下是否有沖突。在 TEX 發行版中,不可以出現名字相同的文件;類似的,宏包名也不可以有重復。

LaTeX 宏包的后綴名是 .sty,因此文件名即為 zhdummy.sty。宏包本身一般不能直接編譯。因此,為了檢查其正確性,還需要加入一些測試文件。我們暫時僅使用一個簡單的 test.tex。這兩個文件目前需要放在同一目錄(不妨設為 zhdummy)下:

zhdummy/
  ├─zhdummy.sty
  └─test.tex

下面我們開始編寫宏包:

% zhdummy.sty
\NeedsTeXFormat{LaTeX2e}
\RequirePackage{expl3}
\ProvidesExplPackage{zhdummy}{2019/11/20}{0.1}{Chinese dummy text (demo)}

\def\mypkgname{zhdummy}

\NeedsTeXFormat{LaTeX2e} 表明宏包要求 LaTeX?2ε 格式,而不接受 plain TeX 和 Con-TeXt 格式。其后的語句之前都介紹過,此處不再贅述。

測試文件則可以這樣寫:

% test.tex
\documentclass{ctexart}
\usepackage{zhdummy}

\begin{document}
你好,\mypkgname{}!
\end{document}

使用 X?ELaTeX 編譯(以后沒有特殊說明,我們將總是使用 X?ELaTeX)test.tex,結果應如下所示:

你好,zhdummy!

此外,在日志文件 test.log 中,應當能找到以下信息:

(./zhdummy.sty
Package: zhdummy 2019/11/20 v0.1 Chinese dummy text (demo)
)

這里 ./zhdummy.sty 表示讀入當前目錄(即 ./)中的 zhdummy.sty 文件,外面的括號會把讀取過程中產生的所有信息(比如這里的宏包版本)包裝起來。如果讀入的文件又調用了新的文件,則會層層嵌套。我們的宏包雖然也調用了 expl3.sty,但之前的 ctexart 文檔類實際上已進行了一次調用,因而宏包中的 \RequirePackage{expl3} 就被忽略了。這與 C/C++ 語言的頭文件調用非常相似。

經過一次編譯,現在的目錄結構將變為這樣:

zhdummy/
  ├─zhdummy.sty    宏包
  ├─test.tex       測試文件
  ├─test.aux       編譯輔助文件
  ├─test.log       編譯日志
  └─test.pdf       生成的 PDF

至此,我們就已經寫好了一個最簡單的宏包。當然,它除了打印自己的名字以外,什么功能都沒有。

加入假文

我們的宏包現在其實只有一行是有實際作用的:

\def\mypkgname{zhdummy}

它定義了一個名為 \mypkgname 的宏,并且可以展開為 zhdummy。實際上,所謂「假文」也無非是這樣一些可展開為文本的宏,只不過文本要更長一點。

在 LaTeX3 中,常規文本很適合用一種稱為記號列表的類型(token lists)存儲。相關函數的前綴是 tl。

記號列表,顧名思義由一系列的記號(token,也稱為字元)組成。而記號,要么是指一個附帶有類別碼的字符(character),要么是一個控制序列。比如,在標準情況下,{\hskip 36 pt} 就是下面的一組記號(下標表示類別碼,? 表示空格,注意 \hskip 后的空格是被忽略掉的):

{1 \hskip控制序列 312 612 ?10 p11 t11 }2

不過,就目前來說,我們可以先忽略這些技術細節。畢竟假文中幾乎只含有漢字、標點和一些字母、數字,它們都是比較「正?!溝畝?,不需要特殊處理。

利用記號列表,我們可以把之前的宏定義改寫為如下形式:

\tl_const:Nn \c_zhdummy_text_i { 天地玄黃,宇宙洪荒。 }

\tl_const:Nn 表示創建一個 tl 常量,并用第二個參數作為其內容。這里還有幾點需要注意:

  1. 常量以 c 開頭

  2. 我們把??槊鹱?zhdummy,通常它應該與宏包名稱一致

  3. 暫時把這一常量設置為公有(以 c_ 而非 c__ 開頭)

  4. 空格在 LaTeX3 語法中是被忽略掉的

  5. 這里用《千字文》僅僅是做一個示范,實際使用的假文會長很多

類似地,我們可以加入更多的假文:

\tl_const:Nn \c_zhdummy_text_i     { 天地玄黃,宇宙洪荒。 }
\tl_const:Nn \c_zhdummy_text_ii    { 日月盈昃,辰宿列張。 }
\tl_const:Nn \c_zhdummy_text_iii   { 寒來暑往,秋收冬藏。 }
\tl_const:Nn \c_zhdummy_text_iv    { 閏馀成歲,律呂調陽。 }
\tl_const:Nn \c_zhdummy_text_v     { 云騰致雨,露結為霜。 }
\tl_const:Nn \c_zhdummy_text_vi    { 金生麗水,玉出昆岡。 }
\tl_const:Nn \c_zhdummy_text_vii   { 劍號巨闕,珠稱夜光。 }
\tl_const:Nn \c_zhdummy_text_viii  { 果珍李柰,菜重芥姜。 }
\tl_const:Nn \c_zhdummy_text_ix    { 海咸河淡,鱗潛羽翔。 }
\tl_const:Nn \c_zhdummy_text_x     { 龍師火帝,鳥官人皇。 }
\tl_const:Nn \c_zhdummy_text_xi    { 始制文字,乃服衣裳。 }
\tl_const:Nn \c_zhdummy_text_xii   { 推位讓國,有虞陶唐。 }
\tl_const:Nn \c_zhdummy_text_xiii  { 吊民伐罪,周發殷湯。 }
\tl_const:Nn \c_zhdummy_text_xiv   { 坐朝問道,垂拱平章。 }
\tl_const:Nn \c_zhdummy_text_xv    { 愛育黎首,臣伏戎羌。 }
\tl_const:Nn \c_zhdummy_text_xvi   { 遐邇壹體,率賓歸王。 }
\tl_const:Nn \c_zhdummy_text_xvii  { 鳴鳳在樹,白駒食場。 }
\tl_const:Nn \c_zhdummy_text_xviii { 化被草木,賴及萬方。 }

使用時,可以直接使用,也可以采用 \tl_use:N 命令:

% test.tex
\documentclass{ctexart}
\usepackage{zhdummy}

\begin{document}
\ExplSyntaxOn
\c_zhdummy_text_i
\tl_use:N \c_zhdummy_text_ii
\ExplSyntaxOff
\end{document}

天地玄黃,宇宙洪荒。日月盈昃,辰宿列張。

這樣的定義方式顯然過于冗長和低效。然而,更嚴重的問題還在于,這樣定義的 tl 變量只能用在 LaTeX3 環境中,直接使用會導致錯誤:

% test.tex
\documentclass{ctexart}
\usepackage{zhdummy}

\begin{document}
\tl_use:N \c_zhdummy_text_i
\end{document}

編譯后得到(中斷時可按 return 鍵繼續)

! Undefined control sequence.
l.6 \tl
  _use:N \c_zhdummy_text_i
?
! Missing $ inserted.
<inserted text>
              $
l.6 \tl_
      use:N \c_zhdummy_text_i
?

LaTeX Warning: Command \c invalid in math mode on input line 6.

! Missing $ inserted.
<inserted text>
              $
l.7 \end{document}
?

_ 在常規的類別碼設置下代表下標,必須用在數學環境中,所以用 _: 所定義的命令不能被 LaTeX 接受。這會給用戶造成了極大的麻煩,顯然有悖于我們編寫宏包的初衷。因此,接下來我們要創建一些用戶層(或文檔層)命令,以區分于編程層。

用戶接口

我們分析一下上面定義的假文命令,可以發現它們都有一些共同點:

  • 前面都是統一的 \c_zhdummy_text_

  • 后面則是小寫羅馬數字,如 i、ii

由此,可以讓用戶輸入所需要的段落號,據此選擇需要的假文。當然,大多數情況下用戶所需要的很可能只是一個簡單的命令。這可以通過默認參數來實現。

我們把 \zhdummy 作為用戶層命令。規定它的用法如下:

\zhdummy
\zhdummy[<序號>]

不帶參數時,輸出前四段假文;帶參數時,輸出指定 <序號> 的假文,其中 <序號> 以阿拉伯數字表示。

實現這一用戶層命令,需要解決以下問題:

  • 阿拉伯數字轉換為(小寫)羅馬數字

  • 拼合命令(控制序列)

  • 以較為可靠方式定義用戶層命令

從頭開始做這些工作并不容易。所幸,LaTeX3 給我們提供了比較良好而易用的框架。以下我們將依次進行介紹。

數字轉換

expl3 所提供的轉換函數是 \int_to_roman:n。顧名思義,這個函數接受一個整型參數,再把它轉換為小寫的羅馬數字。另有 \int_to_Roman:n,很容易就可以猜出它的意思。

我們可以做一些實驗(~ 在 LaTeX3 中表示空格):

\int_to_roman:n { 1 } ~
\int_to_roman:n { 5 } ~
\int_to_roman:n { 4999 } ~
\int_to_Roman:n { 1 } ~
\int_to_Roman:n { 5 } ~
\int_to_Roman:n { 4999 }

結果應當為:

i v mmmmcmxcix I V MMMMCMXCIX

拼合命令

類似于 C 語言的 ##、Python 中的 eval 和 Mathematica 中的 Symbol,expl3 也提供了將「字符串」轉換為命令的手段。

為此,我們先回顧一下之前所講過的參數指定。它位于一個函數的 : 后面,描述了該函數的參數結構?;鏡牟問付ò?n、N、p 等。例如 \tl_use:N,就表示接受一個 token(如一個控制序列)作為參數。

現在介紹一種新的參數指定 c,它表示將參數處理為一個控制序列的名稱。例如,以下幾種寫法是等價的:

\tl_use:N \c_zhdummy_text_i
\tl_use:c { c_zhdummy_text_i }
\tl_use:c { c _ zhdummy _ text _ i }  % 注意空格是忽略掉的

`xparse` 宏包簡介

正所謂「臨門一腳」,我們上面的所有工作最終都要面向用戶。LaTeX3 提供的方案是 xparse 宏包,它可以很方便地聲明用戶層(文檔層)命令。

在代碼底層,程序員應當控制合適的粒度,使得絕大多數函數都只完成單一的工作。因而,底層函數的參數應當是確定的。但在用戶層,需求可以千變萬化,但接口應當盡可能保持統一,這就要求參數形式具有一定的多樣性。這與 C++ 中依靠函數重載實現的所謂 ad hoc 多態有異曲同工之處。

xparse 宏包提供了 \NewDocumentCommand 函數,其語法如下:

\NewDocumentCommand <func> {<arg-spec>} {<code>}
  • <func> 即為我們最終提供給用戶的命令,一般來說它應只包含字母,而不含 _、:、@ 等特殊符號

  • plaintext <arg-spec> 是參數指定(注意與之前 LaTeX3 函數的參數指定相區分),可以是:

  • m:表示標準必?。?strong style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">mandatory)參數,可以是單個 token,或者花括號 {…} 包圍的一組 tokens

  • o:表示標準可?。?strong style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">optional)參數,需用方括號 […] 包圍;若未給出,則返回一個特殊的 -NoValue- 標記

  • O{}:同樣為可選參數,但在未給出時則返回默認值

  • 其他更為復雜的參數指定,以及一些特殊情況,我們將在之后進行介紹

  • 示例如下:

    參數指定輸入值#1#2#3
    m m{foo}{bar}foobar
    o m{foo}-NoValue-foo
    o o m[foo]{bar}foo-NoValue-bar
    m O{default}{foo}foodefault
    m O{default}{foo}[bar]foobar
    m O{default}[bar]報錯

  • <code> 為具體的實現代碼,可以使用 #1、#2 這樣的參數,這和傳統 TEX 編程是一致的

  • 除此之外,xparse 還提供了幾個函數,它們的用法和 \NewDocumentCommand 相同,但含義稍有區別:

    函數<func> 已定義<func> 未定義
    \NewDocumentCommand報錯給出定義
    \RenewDocumentCommand重新定義報錯
    \ProvideDocumentCommand什么也不做給出定義
    \DeclareDocumentCommand重新定義給出定義

上面我們提到,輸入為空時,o 型參數會返回一個特殊的 -NoValue- 標記。這一標記不是簡單的 token list,它必須通過 \IfNoValue(TF) 函數進行判斷:

\IfNoValueTF {<arg>} {<true code>} {<false code>}
\IfNoValueT  {<arg>} {<true code>}
\IfNoValueF  {<arg>} {<false code>}

根據參數 <arg> 是否為 -NoValue-,\IfNoValue(TF) 會決定執行 <true code> 還是 <false code>。

代碼實現

最后,我們把以上分析綜合起來,可以寫出如下的代碼:

% 定義命令 `\zhdummy`,允許帶一個可選參數
\NewDocumentCommand \zhdummy { o }
  {
    % 根據參數 `#1` 是否為 `-NoValue-` 分別進行處理
    \IfNoValueTF {#1}
      {
        % `#1` = `-NoValue-`,即不帶參數
        % 直接使用假文命令
        \tl_use:N \c_zhdummy_text_i
        \tl_use:N \c_zhdummy_text_ii
        \tl_use:N \c_zhdummy_text_iii
        \tl_use:N \c_zhdummy_text_iv
      }
      {
        % `#1` ≠ `-NoValue-`,即帶有可選參數
        % 把 `#1` 轉換為小寫羅馬數字,再拼合成假文命令
        \tl_use:c { c_zhdummy_text_ \int_to_roman:n {#1} }
      }
  }

此時,在 test.tex 中即可按照比較常規的方式來使用假文了:

% test.tex
\documentclass{ctexart}
\usepackage{zhdummy}

\begin{document}
\zhdummy

\zhdummy[1]
\zhdummy[2]
\zhdummy[18]
\end{document}

編譯后得到

天地玄黃,宇宙洪荒。日月盈昃,辰宿列張。寒來暑往,秋收冬藏。閏馀成歲,律呂調陽。云騰致雨,露結為霜。

天地玄黃,宇宙洪荒。日月盈昃,辰宿列張?;徊菽?,賴及萬方。

參考

選自:https://stone-zeng.github.io/2019-11-20-l3tutorial-basic/