自定义环境 (\newenvironment)

如果文档中反复需要把某一段内容用同样的格式包起来,就可以定义自己的 \begin{name}…\end{name}。基础工具是 \newenvironment。如果说宏是自制的“命令”,环境就是它的 块级版本。本页会依次说明环境定义的基础:开始时和结束时分别运行的代码、如何传递参数及其陷阱、如何重新定义既有环境,以及现代的 \NewDocumentEnvironment

用 \newenvironment 定义环境

基本工具是 \newenvironment{name}{⟨begin-def⟩}{⟨end-def⟩}。第一个参数是要创建的环境名(不带反斜杠);后两个参数分别写 开始时运行的代码结束时运行的代码。LaTeX 遇到 \begin{name} 时执行 ⟨begin-def⟩,遇到 \end{name} 时执行 ⟨end-def⟩,夹在两者之间的正文照常排版。

latex
% プリアンブルで定義 / define in the preamble
\newenvironment{warning}{%
  \par\noindent\textbf{注意:}\itshape
}{%
  \par
}

% 本文で使う / use it in the body
\begin{warning}
  この操作は元に戻せません。
\end{warning}

一个关键性质是:环境本身会形成一个隐含的分组,也就是局部作用域。LaTeX 规则规定,“⟨begin-def⟩ 的代码、环境正文以及 ⟨end-def⟩ 的代码都在同一个分组中处理”。因此,在 ⟨begin-def⟩ 中改变字体或间距,会在 \end{name} 后自动恢复,不会泄漏到周围文本。上例中没有显式关闭 \itshape(斜体),却只有环境正文变成斜体,原因就在这里。

这种“自动分组”的性质,在想把字体或间距变化限制在固定范围内时非常方便。如果用宏(\newcommand)做同样的事,就需要自己用 { … } 包住内容来建立分组;在环境中,\begin\end 已经承担了这个角色。

带参数的环境,以及“结束代码看不到参数”的原因

如果每次调用都要改变内容,就给环境加参数。写法与 \newcommand 完全相同:在名称后面的方括号中写 参数个数,在 ⟨begin-def⟩ 中用 #1#2 等引用。形式是 \newenvironment{name}[⟨nargs⟩]{⟨begin-def⟩}{⟨end-def⟩}(从 #1#9,最多 9 个)。

latex
% 見出しの語を引数で受け取る / take the heading word as an argument
\newenvironment{point}[1]{%
  \par\noindent\textbf{#1}\quad
}{%
  \par
}

\begin{point}{結論}
  早めにバックアップを取ること。
\end{point}

此外,和 \newcommand 一样,也可以 把第一个参数设为可选并给出默认值。把方括号写两层,即 \newenvironment{name}[⟨nargs⟩][⟨default⟩]{...}{...},就会让 #1 成为可选参数,默认值为 ⟨default⟩。调用时写 \begin{name}(使用默认值)或 \begin{name}[x](把 #1 设为 x)。同 \newcommand 一样,[⟨nargs⟩] 中的 ⟨nargs⟩包含可选参数在内的总参数数

这里有环境特有、也是最大的陷阱。参数 #1#2 等只能在 ⟨begin-def⟩ 中使用,不能在 ⟨end-def⟩ 中使用。规则也明确说,结束代码“不得包含参数;也就是说,不能在这里使用 #1#2 等”。简单说,执行到 \end{name} 时,参数已经不在手边了。在 ⟨end-def⟩ 中写 #1 不会展开成参数值,而会报错。

如果结束时也需要参数值,标准做法是 趁还在 ⟨begin-def⟩ 中先保存这个值。文本可以存进 盒子(用 \newsavebox 分配,再用 \sbox 填入),也可以让宏通过 \newcommand/\def 记住它,然后在 ⟨end-def⟩ 中取回。由于整个环境是一个分组,在 ⟨begin-def⟩ 中保存的内容会一直活到 ⟨end-def⟩。下面的例子在引用末尾输出出处:用 #1(默认 Shakespeare)接收出处,存到盒子 \quoteauthor,结束时再使用。

latex
\newsavebox{\quoteauthor}
\newenvironment{citequote}[1][Shakespeare]{%
  \sbox\quoteauthor{#1}%   begin-def で引数を保存 / save the arg here
  \begin{quotation}
}{%
  \hspace{1em plus 1fill}---\usebox{\quoteauthor}%   end-def で取り出す
  \end{quotation}%
}

\begin{citequote}
  To be, or not to be.
\end{citequote}

\begin{citequote}[Knuth]
  Premature optimization is the root of all evil.
\end{citequote}

\renewenvironment 与已经存在的名称

环境也有和 \newcommand 相同的安全网:只有名称尚未定义时,\newenvironment 才会成功。把它用于已存在的环境名,会以 LaTeX Error: Command \name already defined. 停止(环境 name 内部由命令 \name 支撑,所以错误信息这样写)。这是有意的设计,用来防止不小心静默覆盖已有环境。

如果要改写既有环境,请使用 \renewenvironment。参数写法(包括 [⟨nargs⟩][⟨default⟩])与 \newenvironment 完全相同;不同的只是行为。\renewenvironment 会在目标 尚未定义时出错,正好是 \newenvironment 的反面。替换标准 quoteitemize 等已有环境的内部内容时会用到它。

latex
% 既存の quote を、本文を斜体にする版へ作り直す
\renewenvironment{quote}{%
  \list{}{\rightmargin\leftmargin}\item\relax\itshape
}{%
  \endlist
}

有几条规则需要遵守。第一,环境名不能以字符串 end 开头\end{...} 的处理会在内部使用名为 \end<name> 的命令,这是为了避免冲突)。第二,\newenvironment\renewenvironment 都有在名称后加 *星号形式,它们对参数末尾空白的处理不同(通常无星号形式就足够)。标准 LaTeX 没有与 \providecommand 对应的“只在不存在时定义”的环境版;需要时请使用下一节的现代接口。

常见模式

实际工作中,自定义环境大致归为三种模式。它们都共享同一个骨架:在 ⟨begin-def⟩ 中做准备,在 ⟨end-def⟩ 中收尾。

  • 用格式包住内容 — 在 ⟨begin-def⟩ 中设置字体、字号或对齐,让正文继承这个样式。由于有分组性,⟨end-def⟩ 即使为空,变化也会自动恢复。
  • 在前后加入空白 — 在 ⟨begin-def⟩ 开头和 ⟨end-def⟩ 末尾放置 \par\medskip 等竖直间距,用上下留白包住正文。
  • 建立在已有环境之上 — 在 ⟨begin-def⟩ 中用 \begin{...} 打开另一个环境,在 ⟨end-def⟩ 中关闭对应的 \end{...}。这是给 quotecenterlist 稍作加工的常见方法。

第三种“建立在已有环境之上”最常见。例如,smallquote 环境复用 quotation,但用较小字号排版;它只需要在 ⟨begin-def⟩ 中启用 \small 并打开 \begin{quotation},再在 ⟨end-def⟩ 中关闭 \end{quotation}。底层环境的行为(缩进和边距)会原样继承。

latex
% 1) 書式で包む / wrap in formatting
\newenvironment{aside}{\par\small\itshape}{\par}

% 2) 前後に空きを入れる / add space around
\newenvironment{spaced}{\par\medskip\noindent}{\par\medskip}

% 3) 既存の環境の上に作る / build on quotation
\newenvironment{smallquote}{%
  \small\begin{quotation}
}{%
  \end{quotation}
}

注意:在 ⟨begin-def⟩ 中打开的环境必须在 ⟨end-def⟩ 中关闭,配对要保持平衡。还应养成在代码行末放 % 的习惯,避免 ⟨begin-def⟩ 末尾或代码行末的 多余空白 混入输出;上面的多行示例在各行末加 % 正是这个原因。

现代的 \NewDocumentEnvironment

现代 LaTeX 还提供另一种更强大的定义方式:\NewDocumentEnvironment{name}{⟨arg-spec⟩}{⟨begin-code⟩}{⟨end-code⟩}。它与宏一侧的 \NewDocumentCommand 对应,原本是 xparse 宏包的功能,但 自 2020 年 10 月起已并入 LaTeX 内核,无需 \usepackage 即可使用。它不写参数个数,而是用 m(必需)、o(可选)、O{default}(带默认值的可选)、s(是否有星号)等 “参数指定(arg-spec)” 字符串声明参数;指定符的细节见“xparse”页面。

这个接口比旧的 \newenvironment 有两个优势。第一,它可以 接收多个可选参数,并正规地处理星号变体。第二,也是决定性的:参数不仅能在 ⟨begin-code⟩ 中使用,也能在 ⟨end-code⟩ 中使用。内核文档明确写道:“⟨begin-code⟩⟨end-code⟩ 都可以访问由 ⟨arg-spec⟩ 定义的参数。” 也就是说,前面保存盒子的麻烦可以省掉。

latex
% #1 を begin でも end でも使える / #1 usable in both begin and end
\NewDocumentEnvironment{citequote}{O{Shakespeare}}{%
  \begin{quotation}
}{%
  \hspace{1em plus 1fill}---#1%
  \end{quotation}%
}

\begin{citequote}[Knuth]
  Premature optimization is the root of all evil.
\end{citequote}

\NewDocumentEnvironment 也有与 \newenvironment 族对应的命令:\RenewDocumentEnvironment(重新定义)、\ProvideDocumentEnvironment(仅在不存在时定义)、\DeclareDocumentEnvironment(不管是否已存在都定义)。写新代码时,因为参数处理更自由,而且结束代码也能看到参数,推荐使用这个现代接口。