マクロツイーター

はてダから移行した記事の表示が崩れてますが、そのうちに直せればいいのに(えっ)

ヤバくない \text 命令を作る話(bxamstext パッケージ)

ステマ

TeX & LaTeX Advent Calendar

2015/12/01 〜 2015/12/25
〜今さら人に聞けない、TeXのキホン〜

☃ TeX & LaTeX アドベントカレンダー 2015 ☃

*  *  *

某カレンダー(↑)の 16 日目はコレだった。

ヤバい \text 命令

つまり、現在の amstext パッケージの \text 命令の実装では、(数式モードの時に)引数のテキストが 4 回実行される(ただし実際に出力に現れるのはそのうちの 1 回だけ)ことになり、これが原因で直感に反する挙動が起こることがある。カウンタ値の増加についてはパッケージで“大概うまくいく対策”が取られているが、この“対策”の想定から外れるケースでは“もっと不可解な挙動”になってしまう恐れがある。

だからヤバくない \text 命令を作りたかったのに

「4 回実行」をやっている限りはこの問題が完全に解決できる見込みはない。何とかして「引数のテキストは 1 回だけ実行される」実装ができないものだろうか。

素朴に考えると次のような考えが思い浮かぶであろう。

それ自身は何も出力しない \mathchoice を使って、「現在の数式スタイルが(D、T、S、SS*1のうちの)どれか」を判定してその結果を“変数”(マクロなど)に保存する。そのあとに、その変数の値による条件分岐を用いて*2「擬似 mathchoice」を行う。

一見簡単な処理のようにみえるが、実はこの「“変数”に保存する」というのが極めて難しい。

%(数式モード中)
\mathchoice % \@mstyle に"代入"する
  {\def\@mstyle{1}}% D
  {\def\@mstyle{2}}% T
  {\def\@mstyle{3}}% S
  {\def\@mstyle{4}}% SS
\ifcase\@mstyle\relax % 実際に分岐する
  \or D \or T \or S \or SS
\fi

元ネタの記事で説明があるように、\mathchoice はサブ数式を構成するので、そこでは代入が局所化された状態(\begingroup〜\endgroup の中と同じ)になる。だから上のコードでは、\mathchoice の実行後には \@mstyle は未定義に戻っている。

局所化されているなら、大域代入を使えばどうか。

\mathchoice % \@mstyle に"代入"する
  {\gdef\@mstyle{1}}% D
  {\gdef\@mstyle{2}}% T
  {\gdef\@mstyle{3}}% S
  {\gdef\@mstyle{4}}% SS

これもダメである。なぜなら、\mathchoice の選択肢は「4 つとも(順に)実行される」からで、\@mstyle の値は必ず「4」になってしまう。そもそも「4 つとも実行して」いる以上、\mathchoice の実際の選択に関する情報はどうやっても外に伝わらない。

実は、TeX の数式組版処理の性質上、「現在の数式スタイルが何か」は“その場では”決まらず、数式全体を見終わった後で初めて決まるものとなっている。\mathchoice が「4 つとも組版しておいて、後で正しいのを選ぶ」という奇妙な仕様を有しているのも、そもそも“後”にならないとどれが正しいかが判断できないからなのである。

それでもヤバくない \text 命令を作りたい

極めて絶望的な状況であるが、実は“抜け道”がある。「2 パス処理」を持ち込めば実現できるのである。

「現在の数式スタイルがどれか」を判定してその結果を補助ファイル(.aux)に保存する。その後に、前回コンパイル時の判定結果の値を用いて、条件分岐で「擬似 mathchoice」を行う。

ここでのポイントは、補助ファイルへの書込を遅延書込(\immediate でない \write)にすることである*3。遅延書込はページを出力する時になって初めて実行されるものなので、「その場では数式スタイルは決まらない」という原則的な制限の影響を受けない。そして実際に、\mathchoice の選択肢の中で遅延書込を行った場合は、その選択肢が実際に採用(出力)された場合にのみ書込が行われる。結果的に、補助ファイルには「数式スタイルが何か」に関する正しい情報が書き込まれることになる。

そしてヤバくない \text 命令を作った

「2 パス処理」といえば、もちろん zref パッケージの出番である。そういうわけで、実際に zref パッケージを利用して、作ってみた。

実装についての解説は後日に措いて、ここでは使い方だけ説明しておく。基本的に、amstext パッケージの代わりにこの bxamstext パッケージを読み込めばよい。

\usepackage{bxamstext}

これで amstext の全ての機能が使える。内部で amstext を読み込んでその実装を修正する、という手順を踏んでいるので、amstext を内部で読み込む他のパッケージ(amsmath 等)とも共存できる。

実際の例をみてみよう。

% XeLaTeX/LuaLaTeX 文書; 文字コードUTF-8
\documentclass[a4paper]{article}
\usepackage{fontspec}
\newfontfamily\fIpxm{IPAexMincho}
\usepackage{bxamstext}
%\usepackage{amstext}
\newcommand\☃{\text{{\fIpxm}%
  \typeout{Snowman's here!}}}% ノイズ
\begin{document}

We have: $S = \Bigl\{\☃^{\☃^{\☃}}\Bigr\}$.

\end{document}

プレアンブルで定義されている \☃ 命令は数式中で ☃ を(安全に)出力するためのものであるが、挙動の違いを見るために、わざと \text の引数に \typeout を入れている。もし amstext パッケージを読み込んでコンパイルした場合は、1 回で正しい結果が得られるが、

Snowman's here!

のメッセージ表示が \☃ 1 つにつき 4 回(文書中に \☃ は 3 つあるので計 12 回)行われることになる。

これに対して、bxamstext を読み込んでコンパイルした場合は、メッセージ表示は \☃ 1 つにつき 1 回(全体で 3 回)しか行われないが、次の警告メッセージが出る。

LaTeX Warning: Label(s) may have changed. Rerun to get cross-references right.

出力をみると、全ての☃が同じサイズになっている。

文書をもう 1 度コンパイルすると警告が消えて、正常な出力が得られる。

*1:これらの記号の意味については元ネタの記事を参照されたい。

*2:if 文による条件分岐では、当然、偽である分岐のテキストは実行されない。

*3:通常、相互参照のために補助ファイルに書く場合には、ページ番号の情報を得るために遅延書込を行う。