2018/04/30

コマンド定義の中で\catcodeを変える

LaTeXではシングルクォート記号がデフォルトで「」になる。Unicodeでいえば、RIGHT SINGLE QUOTATION MARK(U+2019)。この挙動は、本文ならよいのだけど、等幅フォントにするような場面では都合が悪い。等幅フォントのときにみんなが期待するシングルクォートは「'」である。

みんなが期待する挙動なので、当然、そのためのパッケージがすでにある。upquoteパッケージである。しかし、このupquoteパッケージは、\verbコマンドおよびverbatim環境向けに作られている。fancyverb環境やalltt環境でも使えるが、\textttコマンド内の挙動には影響しない。つまり、upquoteパッケージを使っても、\texttt内のシングルクォート記号は「」になる。

\texttt{...}の中でシングルクォート記号を直立タイプ「'」にするにはどうするか。すぐに思いつくのは、「'」をアクティブ文字にして、OT1/cmttの\char13にするという方法。あるいは、textcompパッケージで提供されている\textquotesingleにするという方法。実際、upquoteパッケージの実装もそのようになっている。その方針をそのまま実装するとこうなる。

\let\orgtexttt\texttt
\def\texttt#1{%
  \catcode`'=\active
  \def'{\textquotesingle}
  \orgtexttt{#1}}

もちろん、これは動かない。この\defの中身はすでにTeXの入力ルーチンで読み込まれているので、\def'{\textquotesingle}の時点では「'」がアクティブ文字になっていない。そのため、ふつうの文字である「'」に対して\defすることになってしまうからだ。結果として、「コントロールシーケンスやアクティブ文字であるべき場所にふつうの文字がある」というエラーになる。

! Missing control sequence inserted.

                \inaccessible

こういう場合の常套手段として、「\lowercaseトリック」と呼ばれるものが知られている。このトリックのポイントは、\lowercaseというコマンドの引数が、「すべての文字を小文字として解釈してくれて、しかもカテゴリーコードに関しては無視してくれるような世界」になることにある。そういう世界で、「'」のことを、なにか「\defの1つめの引数に置いても怒られない都合がいいアクティブ文字」の小文字にしておく。幸い、TeXには、そのような都合がいい文字がある。「~」だ。\def~{...}であれば、\def\texttt#1{...}の定義部の中にいきなり書いても怒られない。そこで、「~'の小文字」ということにしておいて、\def~{...}と書けば、この\defにより実際には「'」に対する定義が書けることになる。

あるアルファベットに対する「小文字」を定義するためのプリミティブは\lccodeだ。これを使い、「'」のことを「~の小文字である」ということにして、その文脈で\lowercase{...}内で\def~{\textquotesingle}すればよい。

\def\texttt#1{%
  \catcode`'=\active
  \begingroup
    \lccode`~=`' %
    \lowercase{\endgroup
      \def~}{\textquotesingle}
  \orgtexttt{#1}}

\lccode`~=`'のスコープを定めている\begingroup\endgroupの組に対し、\lowercase{...}のカッコがきちんと入れ子になっていない。あまつさえ\def~{\textquotesingle}にも「}」がまたがってしまっている。気持ち悪いかもしれないけど、これで問題はない。少なくとも、\def~\begingroup\endgroupの中に入れてしまうと、この\defがローカルになってしまうので、「'」がアクティブになるだけなって定義がないままとなりエラーになってしまう。

! Use of ' doesn't match its definition.

まあ、\gdefにすればいいだけだけど。

\def\texttt#1{%
  \catcode`'=\active
  \begingroup
    \lccode`~=`' %
    \lowercase{%
      \gdef~}{\textquotesingle}\endgroup
  \orgtexttt{#1}}

(ところで、\endgroup\lowercase{の直後で問題ないことの根拠は、正直なところよく理解していない。\lowercaseが大文字小文字の対応表をメモリに展開するのが引数を読む前、だからなんだろうなあ。)

さて、\lowercaseトリックのおかげで「'」がアクティブ化される前に「\def'」と書くことを回避できたので、こでれエラーにならずに動く。しかし、挙動は依然としておかしい。ブロック要素で最初に登場する\textttの中では「'」の定義が効いておらず、その後ろに出てくる「'」が、\textttの中だろうが外だろうが\textquotesingleになってしまうのである。

*\texttt{\show'}
> the character '. % \textttの中だけど、文字
...
*\show'
> '=macro:
->\textquotesingle . % \textttの外だけど、アクティブになってる
...
*\texttt{\show'}
> '=macro:
->\textquotesingle . % 2つめの\textttの中ではアクティブに
...

最初に登場する\textttの中で「'」の定義が効かない原因は、すぐにわかった。\textttの引数はすでに読み込まれているから、「'」に対する\catcodeは効いてないのである。\defに対しては\lowercaseトリックのおかげでごまかせたが、この状態では\textttの引数に対して「'」が\textquotesingleとして扱われることはない。

この欠陥に対処するには、\textttの引数が読み込まれるのを遅らせればいい。そういう場合の常套手段は、マクロの展開を二段階にしてサブのほうで引数を読み込ませる、というもの。つまりこんなふうにする。

\let\orgtexttt\texttt
\def\texttt{%
  \begingroup
    \catcode`'=\active
    \x@texttt}
\def\x@texttt#1{%
  \begingroup
    \lccode`~=`' %
    \lowercase{\endgroup
      \def~}{\textquotesingle}
  \orgtexttt{#1}
  \endgroup}

これで、目的のマクロそのものは完成した。

これにて本件は落着といたす、でもいいんだけど、1つ前の定義が「最初に登場する\textttより後ろで、\textttの中だろうが外だろうがシングルクォートが\textquotesingleになってしまう」という挙動だったのが謎い。まったく\textquotesingleにならない、というなら納得できるんだけど、なぜ\textttの引数内で閉じているはずの\catcode`'=\activeが外に漏れてしまっているのか。

どう考えても、オリジナルの\textttの定義に何かある。 しょうがないのでlatex.ltxを覗くと、\textttの定義は事実上こうなっていた。

\DeclareRobustCommand\texttt[1]{%
    \ifmmode
      \nfss@text{\ttfamily #1}%
    \else
      \hmode@bgroup
       \text@command{#1}%
       \ttfamily\check@icl #1\check@icr
       \expandafter
      \egroup
    \fi}

なんなんだよ、この下から3行めの\expandafter。。これのおかげで、\texttt(を再定義したものの中にある\orgtexttt)の引数が読まれるとき、もりもり後ろのほうまで読まれてしまって、\catcode`'=\activeがしばらく先まで有効になってしまっていたっぽい。

この\expandafterは、\textttに限らず、NFSSが提供するフォント変更コマンドすべてに登場する(ようなマクロになっている)。これ、なんのために必要なの?

なにも謎くなかった。先の動作しない\textttの定義で\catcode`'=\activeは閉じてない。ZRさんありがとうございます。

0 件のコメント: