2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

重点解説! イマドキのLaTeXの“命令フック機能”

Posted at

これは「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言語学習者向きの内容”を挟むことがあります。そういう部分には(例によって):sushi:を付けて明示することにします。

お題:“オレ的重要”命令

今回紹介する機能は“LaTeXマクロの作成”に役立つもの、ということでした。従って、具体的にLaTeXマクロを作成する“お題”を考えてみましょう。

いま、文書中に “オレ的重要” という意味のマークアープを導入したいとします。“オレ的重要”は以下のようなテキスト装飾で表すことにします。

  • 既に強調(\emph)の中にいる場合は太字(\textbf)を指定する。
  • それ以外の場合は強調(\emph)を指定する。

LaTeXの強調は引数型命令\emphの他に宣言型命令\em2でも指定できますが、話を簡単にするため\emの使用は考慮外とします。また、\emphがネストする場合も考慮外とします。

この“オレ的重要”のテキスト装飾を行うための(引数型の)命令\myImportantを作ってみましょう。つまり、作るべき命令の仕様は以下の通りです

  • \myImportant{<テキスト>}: “オレ的重要”のテキスト装飾(先述の通り)を施した状態で引数のテキストを出力する。
    \emやネストした\emphと併用した場合の\myImportantの動作は未定義とします。

\emphはLaTeXの標準的な命令ですが、「\emphがどう実装されているか」はもちろんのこと、「\emphがどういう装飾(イタリック・色を変える・点滅させる:astonished:など)を行うのか」は一般に使用する文書クラスに依存することに注意しましょう。ここでは(やや非現実的な設定ですが)\emphの実装・仕様が何であっても\myImportantは仕様通りに動作する必要があるとします。

あるべき姿

イメージを把握しやすいように、\myImportant命令を使用する例を先に挙げておきます。以下のようなテスト文書test.texを用意しました。

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が仕様通りに実装されていれば、以下のような出力結果が得られるはずです。

image-1.png

方針を考えてみる

この問題で最も難しい箇所はやはり「現在\emphの中にいるのかを判定する」ことでしょう。そういう判定を行う命令はLaTeXの標準的な仕様としては規定されていません。

TeX言語:sushi:の知識を使えば「現在のフォントの状態がどうであるか」を調べられますが、そもそも「\emphがどういう装飾を行うか」が不明である以上「フォントのどの状態を調べるか」も不明であるため、判定は不可能です。

そこで、「\emphの中であるか」を表すフラグを自分で用意することにしましょう。その上でフラグの状態が実際の\emphの使用状況と“連動”するようにします。具体的な方針は以下のようになります。

  1. \emphの中であるか」を表すフラグを用意する。
  2. 「フラグを参照して\textbf\emphの適切な方を実行する」ように\myImportantを実装する。
  3. フラグが仕様を満たす、つまり「実際に\emphの中に入っている時だけフラグが真になる」ように何らかの細工を施す

明らかに難しいのが3であり、これを実現するには結局「\emphに“フラグの値を変更する”という動作を追加する」ことは避けられません。このように既存の命令に何かの動作を追加することを(La)TeX界隈では​「フックする」​と呼びます。もちろん「命令をフックする」というのは「命令の実装を改変する」ということなので、旧来のLaTeXの常識に従えば、これを“LaTeXの範囲”で行う(つまり、上記の方針に従って\myImportantを実装する)ことは不可能3です。

とはいっても「命令をフックするだけなら元の命令の実装:sushi:を理解できなくてもよい」という事情があるため、「LaTeXマクロが書けるがTeX言語は知らない」程度のレベルの人の間で「命令をフックする」技法は実際に使われています。その結果として「TeX言語の罠に嵌って事故が起こる」事例もよく見かけます:upside_down:

ところがイマドキの新しいLaTeXでは状況が違います。「既存の命令をフックする」という機能がカーネルでサポートされているのです。これをうまく利用すれば、従来は“LaTeXの範囲”でできなかった\myImportantのような命令が作れるようになるはずです:smiley:

簡単なやつを先に済ませておく

「フックする」の話(手順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}%
  }%
}

参考:sushi::TeX言語する場合

もちろん、TeX言語:sushi:の条件分岐を「ちゃんと知っていて使える」人であれば、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}% フラグを偽にする
}

参考:sushi::旧来のTeX者の方法

この節の内容:sushi:は本題とは無関係なので、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(愚直に再定義バージョン)
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(別命令にコピーバージョン)
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ユーザにも十分に理解しやすい感じになっています。

myimportant.sty(イマドキの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を実際にタイプセットしてみると、以下の出力結果が得られます。

image-1.png

想定通りの結果が得られていることが判ります:blush:

別の \emph で試してみる

“お題の条件”では「元々の\emphの動作が何であっても\myImportantが仕様通りに動く」ことが求められていました。\emphがLaTeX標準と異なっても正常動作することを確認しましょう。

例えば、\emphを「文字色を赤にする」という動作で再定義します。さらに新しい\emphの定義には(\DeclareRobustCommandでなく)xparse系の\RenewDocumentCommandを用いることにします。

text2.tex
% 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}

出力結果は以下の通りです。この場合も想定通りの結果が得られました:blush::blush:

image-2.png

まとめ

イマドキのLaTeXには旧来のLaTeXにはなかった便利な命令がイロイロあります:smiley: 皆さんも、新しいLaTeX命令の使い方をドンドン解説しましょう!:information_desk_person:

  1. 厳密にいうと、LaTeXカーネルの2021-06-01版以降となります。

  2. \emは「2文字からなるフォント命令」ですが、いわゆる(使ってはいけない)「二文字フォント命令」ではなくて正式なLaTeX2eの命令です。

  3. “LaTeXの範囲”においては、特に“改変することが規定”された命令(カウンタ表示の\enumiなど)以外は改変してはいけないからです。

  4. 新しい(2020-10-01以降の)LaTeXカーネルでは“xparse系”の命令は最初から提供されていて、xparseパッケージを読み込む必要はありません。

  5. コメント中に「スイッチ」というTeX用語がありますが、これは「\newifで定義されるif-トークン」のことを指します。

  6. 詳細は省きますが、命令のユースケースから考えて「この命令は“保護付”で定義するのが好ましい」ことにも注意しましょう。

  7. \emphがネストする場合はこれでは失敗しますが、仕様としてネストはサポートしないことにしたのでした。

  8. \DeclareRobustCommandでLaTeX保護付命令を定義する」「\newcommand系命令でオプション引数付の命令を定義する」「xparse系の命令(\NewDocumentCommand等)で命令を定義する」という場合が該当します。

  9. 本記事ではTeX用語の「control sequence」の訳語として​「制御綴」​を用います。
    \emphのカーネルでの元々の定義は以下のように書けます10

  10. ただし実際には\emph命令は\DeclareTextFontCommandによって定義されています。

  11. つまり\命令B\命令Aを定義したのと同じ定義文(\NewDocumentCommand等)で定義したのと等価になります。コピーされるのは「\NewDocumentCommandに与えた定義本体」の範囲のコードであり、そこで参照されている下請けのマクロまでコピーされるわけではありません。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?