学习一门编程语言,如果只了解语法,必然十分枯燥而且并没有什么用。所以,我们准备从一个基本的例子讲起,尽可能地覆盖 LaTeX3 的重要知识。 这里的例子,是一个中文测试文字(乱数假文)宏包。假文的目的是生成大段没有实际含义的文字,常用来测试排版效果。对于西文,TEX 发行版中已经自带了几个宏包,包括 要编写中文测试文字宏包,首先要做一下整体规划与设计: 最简单的实现,只要定义一些含有大量文字的命令,并且能使用户方便使用。 接下来,为了能够改变假文片段的长度,我们就需要用到计数、循环等功能,同时还会引入一些基本的数据结构。 之后,因为面向的是中文测试,我们的宏包还要能够支持多种字符集和编码,这需要通过类别码机制来处理。 最后,作为一个完整的宏包,还需有一套良好的接口和完善的错误提示,以方便用户使用。 当然,如果可能,我们还会考虑介绍一些高级功能,比如: 利用伪随机数生成真正的「乱数」假文。 通过 构建覆盖面足够的测试,完成宏包发布。 考虑到 LaTeX3 仍处于活跃开发状态,建议使用最新的 TEX 发行版。另一方面,开发 LaTeX 宏包与日常写作文档稍有区别,很多时候我们需要通过命令行输出进行调试,而不仅仅是编译 TEX 文档以生成 PDF。所以,建议使用命令行直接编译。为使用 TEX 写作开发的编辑器,如 WinEdt、TeXstudio 等,反而可能不太适用于此。我们的项目会涉及到汉字处理,因此要保证编辑器对多种编码均有良好支持。 我本人使用的是 TeX Live 2019 + Visual Studio Code。 此外,如果要开始一个正式的项目 / 工程,强烈建议建立一个 Git 仓库来管理。具体做法可参阅廖雪峰的 Git 教程。 我们打算把这个宏包取名为 LaTeX 宏包的后缀名是 下面我们开始编写宏包: 测试文件则可以这样写: 使用 XELaTeX 编译(以后没有特殊说明,我们将总是使用 XELaTeX) 你好,zhdummy! 此外,在日志文件 这里 经过一次编译,现在的目录结构将变为这样: 至此,我们就已经写好了一个最简单的宏包。当然,它除了打印自己的名字以外,什么功能都没有。 我们的宏包现在其实只有一行是有实际作用的: 它定义了一个名为 在 LaTeX3 中,常规文本很适合用一种称为记号列表的类型(token lists)存储。相关函数的前缀是 记号列表,顾名思义由一系列的记号(token,也称为字元)组成。而记号,要么是指一个附带有类别码的字符(character),要么是一个控制序列。比如,在标准情况下, 不过,就目前来说,我们可以先忽略这些技术细节。毕竟假文中几乎只含有汉字、标点和一些字母、数字,它们都是比较「正常」的东西,不需要特殊处理。 利用记号列表,我们可以把之前的宏定义改写为如下形式: 常量以 我们把模块名起做 暂时把这一常量设置为公有(以 空格在 LaTeX3 语法中是被忽略掉的 这里用《千字文》仅仅是做一个示范,实际使用的假文会长很多 类似地,我们可以加入更多的假文: 使用时,可以直接使用,也可以采用 天地玄黄,宇宙洪荒。日月盈昃,辰宿列张。 这样的定义方式显然过于冗长和低效。然而,更严重的问题还在于,这样定义的 编译后得到(中断时可按 return 键继续) 我们分析一下上面定义的假文命令,可以发现它们都有一些共同点: 前面都是统一的 后面则是小写罗马数字,如 由此,可以让用户输入所需要的段落号,据此选择需要的假文。当然,大多数情况下用户所需要的很可能只是一个简单的命令。这可以通过默认参数来实现。 我们把 不带参数时,输出前四段假文;带参数时,输出指定 实现这一用户层命令,需要解决以下问题: 阿拉伯数字转换为(小写)罗马数字 拼合命令(控制序列) 以较为可靠方式定义用户层命令 从头开始做这些工作并不容易。所幸,LaTeX3 给我们提供了比较良好而易用的框架。以下我们将依次进行介绍。 我们可以做一些实验( 结果应当为: i v mmmmcmxcix I V MMMMCMXCIX 类似于 C 语言的 为此,我们先回顾一下之前所讲过的参数指定。它位于一个函数的 现在介绍一种新的参数指定 正所谓「临门一脚」,我们上面的所有工作最终都要面向用户。LaTeX3 提供的方案是 在代码底层,程序员应当控制合适的粒度,使得绝大多数函数都只完成单一的工作。因而,底层函数的参数应当是确定的。但在用户层,需求可以千变万化,但接口应当尽可能保持统一,这就要求参数形式具有一定的多样性。这与 C++ 中依靠函数重载实现的所谓 ad hoc 多态有异曲同工之处。 其他更为复杂的参数指定,以及一些特殊情况,我们将在之后进行介绍 示例如下: 除此之外, 上面我们提到,输入为空时, 根据参数 最后,我们把以上分析综合起来,可以写出如下的代码: 此时,在 编译后得到 天地玄黄,宇宙洪荒。日月盈昃,辰宿列张。寒来暑往,秋收冬藏。闰馀成岁,律吕调阳。云腾致雨,露结为霜。 天地玄黄,宇宙洪荒。日月盈昃,辰宿列张。化被草木,赖及万方。 zhlipsum: 中文乱数假文(Lorem ipsum) - The LaTeX3 Interfaces - Part VI - The `l3tl` package: Token lists Part XI - The `l3int` package: Integers The `xparse` package: Document command parser - 确定目标
lipsum
、kantlipsum
和 blindtext
等;而对于中文,则由我本人编写了 zhlipsum
。我们的目标,就是在这几篇教程中,让大家完成一个类似 zhlipsum
的宏包。看过刘海洋《LaTeX 入门》一书的读者就会发现,这实际上正是书中的练习 8.6。DocStrip
和 doc
宏包进行文学编程。准备
开发环境配置
目录结构
zhdummy
。名字当然可以任意取,但确定之前请务必在网上检索一下是否有冲突。在 TEX 发行版中,不可以出现名字相同的文件;类似的,宏包名也不可以有重复。.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 和 ConTeXt 格式。其后的语句之前都介绍过,此处不再赘述。% test.tex
\documentclass{ctexart}
\usepackage{zhdummy}
\begin{document}
你好,\mypkgname{}!
\end{document}test.tex
,结果应如下所示: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
。实际上,所谓「假文」也无非是这样一些可展开为文本的宏,只不过文本要更长一点。tl
。{\hskip 36 pt}
就是下面的一组记号(下标表示类别码,␣
表示空格,注意 \hskip
后的空格是被忽略掉的):{
1 \hskip
控制序列 3
12 6
12 ␣
10 p
11 t
11 }
2\tl_const:Nn \c_zhdummy_text_i { 天地玄黄,宇宙洪荒。 }
\tl_const:Nn
表示创建一个 tl
常量,并用第二个参数作为其内容。这里还有几点需要注意:c
开头zhdummy
,通常它应该与宏包名称一致c_
而非 c__
开头)\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}! 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[<序号>]<序号>
的假文,其中 <序号>
以阿拉伯数字表示。数字转换
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 }拼合命令
##
宏、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` 宏包简介
xparse
宏包,它可以很方便地声明用户层(文档层)命令。xparse
宏包提供了 \NewDocumentCommand
函数,其语法如下:\NewDocumentCommand <func> {<arg-spec>} {<code>}
<func>
即为我们最终提供给用户的命令,一般来说它应只包含字母,而不含 _
、:
、@
等特殊符号plaintext <arg-spec>
是参数指定(注意与之前 LaTeX3 函数的参数指定相区分),可以是:m
:表示标准必选(mandatory)参数,可以是单个 token,或者花括号 {…}
包围的一组 tokenso
:表示标准可选(optional)参数,需用方括号 […]
包围;若未给出,则返回一个特殊的 -NoValue-
标记O{
:同样为可选参数,但在未给出时则返回默认值 参数指定 输入值 #1
#2
#3
m m
{foo}{bar}
foo
bar
o m
{foo}
-NoValue-
foo
o o m
[foo]{bar}
foo
-NoValue-
bar
m O{default}
{foo}
foo
default
m O{default}
{foo}[bar]
foo
bar
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}参考
texdoc zhlipsum.pdf
texdoc interface3
texdoc xparse
选自:https://stone-zeng.github.io/2019-11-20-l3tutorial-basic/
发表评论 取消回复