これは「TeX & LaTeX Advent Caleandar 2024」の25日目の記事です。
(24日目は k16shikano さんです。)
LaTeXカーネルの“凍結解除”が宣言されたのが2015年の1月のことなので、それからほぼ10年の年月が経ちました。この10年の間に数多くの機能がLaTeXカーネル(パッケージではなく)に追加されていることは最近のTeXConf(やOnline.tex)をチェックしている人ならご存じでしょう。
一方で、日本語のLaTeX界隈において、そういう“新しいカーネルの機能”についての具体的な使い方の解説が積極的に流布されているかというと、残念ながらそういう活動は停滞気味です。結果的に、カーネルの新機能があっても「使い方を知らないので使えない」状態になっています。
本記事では、カーネルの新機能のうち、“LaTeXマクロを作成する”程度のレベルの人にとって有用な「命令に対するフック」について解説します。
本記事の内容はTeX Live 2022以降のLaTeX1を前提とします。
前提知識
-
LaTeXの範囲のマクロ(ユーザ定義命令)の作成ができること。
- 記事全体の方針としてはTeX言語の知識は仮定しない。
ただし、本記事では部分的に“TeX言語学習者向きの内容”を挟むことがあります。そういう部分には(例によって)を付けて明示することにします。
お題:“オレ的重要”命令
今回紹介する機能は“LaTeXマクロの作成”に役立つもの、ということでした。従って、具体的にLaTeXマクロを作成する“お題”を考えてみましょう。
いま、文書中に “オレ的重要” という意味のマークアープを導入したいとします。“オレ的重要”は以下のようなテキスト装飾で表すことにします。
- 既に強調(
\emph
)の中にいる場合は太字(\textbf
)を指定する。 - それ以外の場合は強調(
\emph
)を指定する。
LaTeXの強調は引数型命令\emph
の他に宣言型命令\em
2でも指定できますが、話を簡単にするため\em
の使用は考慮外とします。また、\emph
がネストする場合も考慮外とします。
この“オレ的重要”のテキスト装飾を行うための(引数型の)命令\myImportant
を作ってみましょう。つまり、作るべき命令の仕様は以下の通りです
-
\myImportant{<テキスト>}
: “オレ的重要”のテキスト装飾(先述の通り)を施した状態で引数のテキストを出力する。
※\em
やネストした\emph
と併用した場合の\myImportant
の動作は未定義とします。
\emph
はLaTeXの標準的な命令ですが、「\emph
がどう実装されているか」はもちろんのこと、「\emph
がどういう装飾(イタリック・色を変える・点滅させるなど)を行うのか」は一般に使用する文書クラスに依存することに注意しましょう。ここでは(やや非現実的な設定ですが)\emph
の実装・仕様が何であっても\myImportant
は仕様通りに動作する必要があるとします。
あるべき姿
イメージを把握しやすいように、\myImportant
命令を使用する例を先に挙げておきます。以下のようなテスト文書test.texを用意しました。
% pdfLaTeX文書
\documentclass[a4paper]{article}
\usepackage{myimportant} % ここに'\myImportant'の実装がある
\begin{document}
% '\emph'の外で使う場合
The {\TeX} language is \myImportant{dangerous}.
% '\emph'の中で使う場合
\emph{The {\TeX} language is \myImportant{dangerous}.}
\end{document}
この中で読み込んでいるmyimportant
パッケージに\myImportant
命令の実装が記述されているものとします。標準のarticleクラスでは\emph
命令はイタリックで表現されるので、\myImportant
が仕様通りに実装されていれば、以下のような出力結果が得られるはずです。
方針を考えてみる
この問題で最も難しい箇所はやはり「現在\emph
の中にいるのかを判定する」ことでしょう。そういう判定を行う命令はLaTeXの標準的な仕様としては規定されていません。
TeX言語の知識を使えば「現在のフォントの状態がどうであるか」を調べられますが、そもそも「\emph
がどういう装飾を行うか」が不明である以上「フォントのどの状態を調べるか」も不明であるため、判定は不可能です。
そこで、「\emph
の中であるか」を表すフラグを自分で用意することにしましょう。その上でフラグの状態が実際の\emph
の使用状況と“連動”するようにします。具体的な方針は以下のようになります。
- 「
\emph
の中であるか」を表すフラグを用意する。 - 「フラグを参照して
\textbf
と\emph
の適切な方を実行する」ように\myImportant
を実装する。 - フラグが仕様を満たす、つまり「実際に
\emph
の中に入っている時だけフラグが真になる」ように何らかの細工を施す。
明らかに難しいのが3であり、これを実現するには結局「\emph
に“フラグの値を変更する”という動作を追加する」ことは避けられません。このように既存の命令に何かの動作を追加することを(La)TeX界隈では「フックする」と呼びます。もちろん「命令をフックする」というのは「命令の実装を改変する」ということなので、旧来のLaTeXの常識に従えば、これを“LaTeXの範囲”で行う(つまり、上記の方針に従って\myImportant
を実装する)ことは不可能3です。
とはいっても「命令をフックするだけなら元の命令の実装を理解できなくてもよい」という事情があるため、「LaTeXマクロが書けるがTeX言語は知らない」程度のレベルの人の間で「命令をフックする」技法は実際に使われています。その結果として「TeX言語の罠に嵌って事故が起こる」事例もよく見かけます
ところがイマドキの新しいLaTeXでは状況が違います。「既存の命令をフックする」という機能がカーネルでサポートされているのです。これをうまく利用すれば、従来は“LaTeXの範囲”でできなかった\myImportant
のような命令が作れるようになるはずです
簡単なやつを先に済ませておく
「フックする」の話(手順3)に入る前に、その他の部分である1と2を片付けておきましょう。“LaTeXの範囲”で簡単な条件分岐を行いたいのでifthenパッケージを利用することにします。
フラグを定義する
ifthenパッケージでは\newboolean
命令でフラグ(真偽値変数)を定義できます。「\emph
の中であるか」を表すフラグの名前をmyInEmph
としましょう。
% 変数'myInEmph': 今'\emph'の中であるか.
\newboolean{myInEmph}
フラグの値により条件分岐する
条件分岐をするのに使うのは\ifthenelse
命令です。この条件部で\boolean{myInEmph}
と書くことで先ほど定義したmyInEmph
の現在の値を参照できます。これを利用して、\myImportant
命令の定義は以下のように書けます。
イマドキのLaTeXの話なので、マクロ定義にも\newcommand
ではなく新しい“xparse系”の命令4である\NewDocumentCommand
を使うことにしましょう。
% \myImportant{<テキスト>}: "オレ的重要"の装飾付きでテキストを出力する.
\NewDocumentCommand\myImportant{m}{%
\ifthenelse{\boolean{myInEmph}}{%
% フラグmyInEmphが真の場合の処理
\textbf{#1}%
}{%else
% myInEmphが偽の場合の処理
\emph{#1}%
}%
}
参考:TeX言語する場合
もちろん、TeX言語の条件分岐を「ちゃんと知っていて使える」人であれば、ifthenパッケージの代わりにTeXのif文を使ってもかまいません。例えば以下のようなコードになります5。
\myImportant
は公開の命令なので6、\def
ではなく\NewDocumentCommand
で定義すべきです。
% スイッチ'my@in@emph': 今'\emph'の中であるか.
\newif\ifmy@in@emph
% \myImportant{<text>}: "オレ的重要"の装飾付きでテキストを出力する.
\NewDocumentCommand\myImportant{m}{%
\ifmy@in@emph
\textbf{#1}%
\else
\emph{#1}%
\fi
}
以降ではifthenを用いたコードの方を採用します。
\emph をフックする方針
いよいよ本題の「\emph
をフックする」実装について検討しましょう。
求める動作は「\emph
の中である場合にだけフラグmyInEmph
が真になる」ということです。これを実現する簡単な方法は、\emph
の動作の前後にそれぞれ「myInEmph
を真にする」「myInEmph
を偽にする」という動作を追加することです7。概念的なコードで表すと以下のようになります。
\RenewDocumentCommand\emph{m}{% 再定義する
\setboolean{myInEmph}{true}% フラグを真にする
"従来の\emph"{#1}%
\setboolean{myInEmph}{false}% フラグを偽にする
}
参考:旧来のTeX者の方法
この節の内容は本題とは無関係なので、LaTeX者の皆さんは丸ごとスキップしてかまいません。
新しい方法を紹介する前に、旧来のTeX言語で使われてきた手法を復習しておきます。
愚直に再定義する方法
「\emph
をフックする」ための最も愚直な方法は「\emph
の定義を調べて、その上で動作を追加した形で再定義する」というものです。
latex null
を実行してLaTeXを対話モードで起動した後で、\ShowCommand
命令を利用して\emph
の定義を調べます。
\ShowCommand
は「プリミティブ\show
のLaTeX用拡張版」に相当する新しいLaTeX命令で、“LaTeXの命令定義がTeXのマクロ定義に単純には対応しない”場合8にも適切な定義本体のコードが出力されます。
*\ShowCommand\emph
> \emph=robust macro:
->\protect \emph␣ .
> \emph␣=\long macro:
#1->\ifmmode \nfss@text {\em #1}\else \hmode@bgroup \text@command {#1}\em \chec
k@icl #1\check@icr \expandafter \egroup \fi .
ここから、\emph
はLaTeX保護付の(\DeclareRobustCommand
で定義された)命令であることがわかります。
\emph
のLaTeX保護付命令の実行は「emph␣
(末尾に空白)という名前の制御綴9」に移譲されます。この制御綴を\emph␣
と書くことにします。上の画面でこの制御綴を表す\emph
の表示の部分を敢えて\emph␣
と書きました。
% '\emph'の元々の定義.
\DeclareRobustCommand\emph[1]{%
\ifmmode
\nfss@text{\em #1}%
\else
\hmode@bgroup
\text@command{#1}\em
\check@icl#1\check@icr
\expandafter\egroup
\fi
}
ここまで把握できれば\emph
を実際に再定義するのは簡単です。先頭と末尾に\setboolean
のコードを追加すればいいだけです。
% '\emph'を再定義する.
\DeclareRobustCommand\emph[1]{%
\setboolean{myInEmph}{true}%←追加
\ifmmode
\nfss@text{\em #1}%
\else
\hmode@bgroup
\text@command{#1}\em
\check@icl#1\check@icr
\expandafter\egroup
\fi
\setboolean{myInEmph}{false}%←追加
}
手順1~3で記述したコードをまとめると\myImportant
の実装が完成します。
myimportant.sty(愚直に再定義バージョン)
\RequirePackage{ifthen}
% 変数'myInEmph': 今'\emph'の中であるか.
\newboolean{myInEmph}
% \myImportant{<テキスト>}: "オレ的重要"の装飾付きでテキストを出力する.
\NewDocumentCommand\myImportant{m}{%
\ifthenelse{\boolean{myInEmph}}{%
% フラグmyInEmphが真の場合の処理
\textbf{#1}%
}{%else
% myInEmphが偽の場合の処理
\emph{#1}%
}%
}
% '\emph'を再定義する.
\DeclareRobustCommand\emph[1]{%
\setboolean{myInEmph}{true}%←追加
\ifmmode
\nfss@text{\em #1}%
\else
\hmode@bgroup
\text@command{#1}\em
\check@icl#1\check@icr
\expandafter\egroup
\fi
\setboolean{myInEmph}{false}%←追加
}
このパッケージファイルmyimportant.styを用意してテスト文書test.texを実際にタイプセットすると想定通り(冒頭の画像で示した)の出力結果が得られます。ということは、これで「お題」を解決したことになるのでしょうか?
残念ながら違います。なぜなら、お題では「事前には\emph
の仕様・実装が不明である」という条件が付いているからです。「現在の仕様に基づいて新しい実装を作る」という愚直な方法はそもそも通用しないのでした。
別の命令にコピーする方法
既存の命令を「フックする」ための他の手法として「既存の命令を別の名前の命令にコピーする」というものがあります。前節で示した\emph
の再定義の“概念的コード”を改めて見てみましょう。
\RenewDocumentCommand\emph{m}{% 再定義する
\setboolean{myInEmph}{true}% フラグを真にする
"従来の\emph"{#1}%
\setboolean{myInEmph}{false}% フラグを偽にする
}
もちろんこのコードはそのままでは使えませんが、もし「従来の\emph
」の同等の動作をする命令が別の名前、例えば\my@org@emph
で定義されているとすれば、その\my@org@emph
を使って\emph
が素直に再定義できるはずです。
% '\emph'を再定義する.
\RenewDocumentCommand\emph{m}{%
\setboolean{myInEmph}{true}% フラグを真にする
% '\my@org@emph'が従来の'\emph'と等価とする
\my@org@emph{#1}%
\setboolean{myInEmph}{false}% フラグを偽にする
}
ということは、上の再定義を行う前に「\emph
を\my@org@emph
にコピーする」という手順を行えばよいことになります。
ところがここで問題があります。それは「命令を確実にコピーするのは案外難しい」ということです。一見すると、単純にTeXプリミティブの\let
を使えばいいように見えます。
% 命令をコピーする
\let\my@org@emph\emph
しかしこれは正しくありません。先述の通り、\emph
は\DeclareRobustCommand
で定義されたLaTeX保護付命令であるため、“\emph
命令の定義”の実体は実際には\emph␣
というTeXマクロに定義されているからです。従って「\emph
マクロを\my@org@emph
マクロにコピーする」とともに「\emph␣
マクロを\my@org@emph␣
マクロにコピーする」という手順を行わないとコピー先の\my@org@emph
命令が正常に動作しないのです。結局「\emph
命令を\my@org@emph
命令にコピーする」コードは以下のようになります。
% '\emph'マクロを'\my@org@emph'にコピー
\let\my@org@emph\emph
% '\emph␣'マクロを'\my@org@emph␣'にコピー
\ExpandArgs{cc}\let{my@org@emph }{emph }
\ExpandArgs
は「命令の引数を制御綴名から制御綴に転換させる」という機能をもった新しいLaTeX命令です。\ExpandArgs{cc}
はちょうどexpl3の\exp_args:Ncc
と同じ働きをします。
これで一応動くようになるのですが、ここで再び“お題の条件”が問題になります。カーネル標準の\emph
は確かにLaTeX保護付命令なのですが、お題の条件では\emph
の特定の実装を前提にできません。すると、そもそも\emph
が“どのように定義されたLaTeX命令”かも不明となり、「\emph
命令を\my@org@emph
命令に確実にコピーする」手順も不明になりそうです。
ところが実は、新しいLaTeXには「LaTeXの命令をコピーする」ための機能が備わっています。それが\DeclareCommandCopy
命令です。
-
\DeclareCommandCopy\命令A\命令B
: LaTeXの命令\命令B
を\命令A
にコピーする。\命令A
が“TeXのマクロ定義に単純には対応しない”方式で定義された場合でも正しくコピーされる11。
この\DeclareCommandCopy
を使えば「命令のコピー」の問題が完全に解決できます。
% '\emph'命令の現在の定義を'\my@org@emph'にコピー
\DeclareCommandCopy\my@org@emph\emph
以上のコードをまとめると“お題の条件”を満たす\myImportant
の実装が完成します。
myimportant.sty(別命令にコピーバージョン)
\RequirePackage{ifthen}
% 変数'myInEmph': 今'\emph'の中であるか.
\newboolean{myInEmph}
% \myImportant{<テキスト>}: "オレ的重要"の装飾付きでテキストを出力する.
\NewDocumentCommand\myImportant{m}{%
\ifthenelse{\boolean{myInEmph}}{%
% フラグmyInEmphが真の場合の処理
\textbf{#1}%
}{%else
% myInEmphが偽の場合の処理
\emph{#1}%
}%
}
% '\emph'命令の現在の定義を'\my@org@emph'にコピー
\DeclareCommandCopy\my@org@emph\emph
% '\emph'を再定義する.
\RenewDocumentCommand\emph{m}{%
\setboolean{myInEmph}{true}% フラグを真にする
\my@org@emph{#1}%
\setboolean{myInEmph}{false}% フラグを偽にする
}
LaTeXの“命令フック機能”を使う方法
さて、かなり遠回りしてきましたが、いよいよ本題の「イマドキのLaTeXの命令フック機能」に話を移しましょう。
命令フック機能
新しいLaTeXで提供される命令フック機能は、任意の命令を対象にして、その先頭および末尾に新たな動作を追加することを可能にします。
-
\AddToHook{cmd/‹命令名›/before}{‹コード›}
: LaTeXの命令\‹命令名›
について、その先頭で‹コード›
が実行されるようにする。 -
\AddToHook{cmd/‹命令名›/after}{‹コード›}
: LaTeXの命令\‹命令名›
について、その末尾で‹コード›
が実行されるようにする。
もちろん、対象の命令が“TeXのマクロ定義に単純には対応しない”方式で定義されたものでも正しく動作します。
命令の代わりに環境を対象とした形式もあります。
-
\AddToHook{env/‹環境名›/before}{‹コード›}
: 指定の名前のLaTeXの環境について、その先頭(グルーピングに入る前)で‹コード›
が実行されるようにする。 -
\AddToHook{env/‹環境名›/after}{‹コード›}
: 指定の名前のLaTeXの環境について、その末尾(グルーピングを出た後)で‹コード›
が実行されるようにする。
つまり、新しいLaTeXの命令フック機能は、従来「命令の別名へのコピー」で対応していた「命令のフック」という操作を、もっと安全でかつ直感的な方法で扱えるようにしたものといえるでしょう。
イマドキの方法でフックする
それでは早速、イマドキの命令フック機能を利用して“お題”の手順3を実装してみましょう。「\emph
命令の先頭と末尾に必要な\setboolean
のコードを追加したい」ので以下のようになります。
% '\emph'にフックを施す.
\AddToHook{cmd/emph/before}{%
\setboolean{myInEmph}{true}%
}
\AddToHook{cmd/emph/after}{%
\setboolean{myInEmph}{false}%
}
フック機能はまさに“フックするための機能”であるため、極めて直接的に要件が実現できることが判ります。
手順1~3のコードをまとめたものは以下の通りです。全体としてLaTeXユーザにも十分に理解しやすい感じになっています。
\RequirePackage{ifthen}
% 変数'myInEmph': 今'\emph'の中であるか.
\newboolean{myInEmph}
% \myImportant{<テキスト>}: "オレ的重要"の装飾付きでテキストを出力する.
\NewDocumentCommand\myImportant{m}{%
\ifthenelse{\boolean{myInEmph}}{%
% フラグmyInEmphが真の場合の処理
\textbf{#1}%
}{%else
% myInEmphが偽の場合の処理
\emph{#1}%
}%
}
% '\emph'にフックを施す.
\AddToHook{cmd/emph/before}{%
\setboolean{myInEmph}{true}%
}
\AddToHook{cmd/emph/after}{%
\setboolean{myInEmph}{false}%
}
このパッケージファイルmyimportant.styを用意してテスト文書test.texを実際にタイプセットしてみると、以下の出力結果が得られます。
想定通りの結果が得られていることが判ります
別の \emph で試してみる
“お題の条件”では「元々の\emph
の動作が何であっても\myImportant
が仕様通りに動く」ことが求められていました。\emph
がLaTeX標準と異なっても正常動作することを確認しましょう。
例えば、\emph
を「文字色を赤にする」という動作で再定義します。さらに新しい\emph
の定義には(\DeclareRobustCommand
でなく)xparse系の\RenewDocumentCommand
を用いることにします。
% pdfLaTeX文書
\documentclass[a4paper]{article}
\usepackage{xcolor} % '\color'のため
% '\emph'の定義を変更してみる.
\RenewDocumentCommand\emph{+m}{%
{\color{red}#1}% 文字色を赤にする
}
% (これ以降はtest.texと同じ)
\usepackage{myimportant}
\begin{document}
% '\emph'の外で使う場合
The {\TeX} language is \myImportant{dangerous}.
% '\emph'の中で使う場合
\emph{The {\TeX} language is \myImportant{dangerous}.}
\end{document}
出力結果は以下の通りです。この場合も想定通りの結果が得られました
まとめ
イマドキのLaTeXには旧来のLaTeXにはなかった便利な命令がイロイロあります 皆さんも、新しいLaTeX命令の使い方をドンドン解説しましょう!
-
厳密にいうと、LaTeXカーネルの2021-06-01版以降となります。 ↩
-
\em
は「2文字からなるフォント命令」ですが、いわゆる(使ってはいけない)「二文字フォント命令」ではなくて正式なLaTeX2eの命令です。 ↩ -
“LaTeXの範囲”においては、特に“改変することが規定”された命令(カウンタ表示の
\enumi
など)以外は改変してはいけないからです。 ↩ -
新しい(2020-10-01以降の)LaTeXカーネルでは“xparse系”の命令は最初から提供されていて、xparseパッケージを読み込む必要はありません。 ↩
-
コメント中に「スイッチ」というTeX用語がありますが、これは「
\newif
で定義されるif-トークン」のことを指します。 ↩ -
詳細は省きますが、命令のユースケースから考えて「この命令は“保護付”で定義するのが好ましい」ことにも注意しましょう。 ↩
-
\emph
がネストする場合はこれでは失敗しますが、仕様としてネストはサポートしないことにしたのでした。 ↩ -
「
\DeclareRobustCommand
でLaTeX保護付命令を定義する」「\newcommand
系命令でオプション引数付の命令を定義する」「xparse系の命令(\NewDocumentCommand
等)で命令を定義する」という場合が該当します。 ↩ -
本記事ではTeX用語の「control sequence」の訳語として「制御綴」を用います。
\emph
のカーネルでの元々の定義は以下のように書けます10。 ↩ -
ただし実際には
\emph
命令は\DeclareTextFontCommand
によって定義されています。 ↩ -
つまり
\命令B
は\命令A
を定義したのと同じ定義文(\NewDocumentCommand
等)で定義したのと等価になります。コピーされるのは「\NewDocumentCommand
に与えた定義本体」の範囲のコードであり、そこで参照されている下請けのマクロまでコピーされるわけではありません。 ↩