この記事は TeX & LaTeX Advent Calendar 2014 の 15 日目の記事です. 昨日(12/14)は hak7a3 さん,明日(12/16)は CardinalXaro さんです.
TeX ユーザの集い 2014 の休憩中, 次のように pLaTeX で記述すると,全角カンマと開き括弧の間には全角空きが挿入されてしまうという挙動が話題になりました.
この記事では,上記の挙動や,解決の助けになることを期待して実装した \lastnodechar プリミティブ について解説します.
\lastnodechar プリミティブは, TeX Live 開発版のソースに r35619 (2014/11/19) にコミットされたものが「まともに使えるはず」のものです. 従って,
また,\lastnodechar プリミティブは, e-拡張が有効になっている e-pTeX, e-upTeX にのみ実装されています. コマンド名で言えば,
さて,pTeX 系列(ASCII pTeX, upTeX, e-pTeX, e-upTeX)では,和文文字間に入る空白について,次のような仕様になっています.
それぞれの後半部の「」の量は,フォントメトリックによって決められています.
上の説明では何を述べているかわかりづらいと思うので,具体例を述べます. 例えば,pLaTeX2e 新ドキュメントクラス などで使われる JIS フォントメトリックでは,次のようになっています.
このとき,次の入力を考えてみましょう.
すると,次のようになります.
上の説明には 2 点補足があります.
特に今の場合重要になるのは前者で,例えば次の1行目のように入力すると, 「,」「『」の間には合計で全角空きが挿入されることになります.
なぜなら以下のように,「,」「『」それぞれに由来する半角空きが挿入されるからです.
マクロを使った例も載せてみます.
上の場合,ソース中で「,」の次にあるのは制御綴 \hoge ですが, それは「『」へと展開されます.結果として,「,」の直後の展開不能トークンは「『」ということとなり,両者の間には 半角空きが入ることとなります.
pTeX 系列での「段落開始時の開き括弧が全角二分下がりになる」のもこれで説明がつきます.
最初の「『」の直前は和文文字ではないので, 1. のケースとなり,「『」の直前には半角空きが入る,というわけです.
以上を元に,本記事冒頭の例について見ていくことにします.
例えば新ドキュメントクラスを利用している場合に,\textgt の定義を \show\textgt によって調べると次のようになります.
> \textgt=macro: #1->\relax \ifmmode \hbox \fi {\gtfamily #1}.言い換えれば,本記事冒頭の例は,次のように書いたのとほぼ同じことになります.
よって次の2点がわかります.
結果として「,」由来の半角空き,「『」由来の半角空きの両方が「,」と「『」の間に挿入される,というわけです.
pLaTeX 標準クラス jarticle.cls などでは \textgt の定義が異なりますが,状況としては同様で, 「,」由来・「『」由来の両方の空きが「,」と「『」の間に挿入されることになります.
ここまでが前振りです.
「望ましくない全角空きが入る」問題解消のためにまず考えるのは和文間のグルー挿入を抑止する \inhibitglue を挿入するということです. 例えば以下のコードでは,「『」由来の半角空き・「,」由来の半角空きの挿入を抑止することで,両者の間が半角空きになるようにしています.
しかし,自動で一律に \inhibitglue を入れるのは望ましくありません. 以下の例では,入ってほしいはずの半角空きがなくなります.
というわけで,空きが正しくなるような「\textgt 改」とでも言うべき命令を作るためには, 命令の直前の文字と,命令の引数の最初の和文文字を読み取り,それに応じて処理を分岐させる必要が出てきます.
しかし,TeX の処理方式により,今まではそれが難しいことでした.TeX by Topic の 第 1.1 節などに説明がありますが,入力されたソースファイルは次の 4 段階で処理され,前の段階へと逆流することはありません.
本記事の最初から出している例を振り返ってみます.
この中で,\textgt を展開しよう,という段になってみると,その直前の「,」は既に実行が終わっている段階なので, \textgt の中で直前の「,」を(トークンとして)知ることは出来ない,というわけです.
別の言葉で述べますと,TeX では文字・空白,罫線などは最終的にノードという形になり, 段落やページの中身はノード達のリンクリストとして表現されることになります. 第 3 段階の「胃」はトークンをノードに変換する過程であり,実行が終わって 一度ノードに変換されたものはトークンに戻せないわけです.
これが,私が \lastnodechar プリミティブを必要だと考えたわけになります. 「直前の文字」をトークンとして知ることが出来ないにしても,ノードに変換されているはずだから, そこから「直前の文字」についての情報を得ることはできるはずだ,ということです.
一言で説明すれば,次のようになります: \lastnodechar は,現在構築中のリスト(ボックス,段落,ページ等)中の「最後のノード」が文字由来であればその文字コードを,そうでなければ -1 をそれぞれ内部整数として返す.
簡単なサンプルは以下の通りです.
上記の説明で述べた「最後のノード」については注意があります. 以下,次の 2 つの命令を用いながら説明していきます.
なお,以下の例において,クラスファイルは jsarticle を使用しています.
\lastnodechar は「最後のノード」の情報を得るものなので,ノード生成に寄与しないもの,例えば \relax,空グループ {},レジスタへの代入処理などで 結果が変わることはありません.例えば,次のコードからは 4 行とも「e」の文字コード 101 が得られます.
pTeX 系列では欧文のベースライン補正が \ybaselineshift(横組),\tbaselineshift(縦組)でできるようになっています. これらが 0 でない場合,最後のノードはベースライン補正用のノードになります. 例えば,次のコードからは,以下の内容が得られます.
foo\showlists
\hbox(0.0+0.0)x9.24687 % \parindent 由来 \displace 2.0 \OT1/cmr/m/n/10 f \OT1/cmr/m/n/10 o \kern0.27779 \OT1/cmr/m/n/10 o \displace 0.0
\displace x.y というノードがベースライン補正用のノード*1です. しかしこれは自動挿入されるものなので,\lastnodechar はそれを飛ばし,きちんと「o」の文字コード 111 を返します.
また,LuaTeX 以外の TeX では欧文の合字はまとめて 1 つのノードとして扱われます.例えば以下のコードからは, リガチャ「ffi」が 1 つのノードにまとめられていることがわかります.
\hbox(0.0+0.0)x9.24687 % \parindent 由来 \OT1/cmr/m/n/10 ^^N (ligature ffi)
この場合,\lastnodechar は,リガチャの最後の構成要素の文字コードを返す ことにしました.
これは,リガチャの周囲の和欧文間空白 (\xkanjiskip) の挿入判定を,直前の和文文字と「最初の構成要素」の間,「最後の構成要素」と直後の和文文字の間で 行うという仕様に合わせたものです.
JIS フォントメトリックにおける閉じ括弧類や読点・句点など,「その文字と『文字クラス 0 の文字』との間に空白が入る」ことがフォントメトリックによって 決まっている場合があります.
e-拡張で定義されている「最後のノード」の種類を返す \lastnodetype というプリミティブでは,その空白を拾います.
しかし,\lastnodechar は実装目的からしてこのような動作は望ましくないので, このようなメトリック由来の空白を \lastnodechar は無視します.言い換えれば, 以下のソースは期待されたとおりに「』」の内部コードを返す,というわけです.
例として,次で定義される \fixjfmspacing, \mytextgt 命令を考えてみます.
- \makeatletter
- \let\fjs@kanji=漢% kanji
- \let\fjs@kana=あ% kana
- \let\fjs@other=(% other
- \def\fixjfmspacing{%
- % \@temp は \fixjfmspacing 直後のトークンとなる
- \futurelet\@temp\fixjfmspacing@
- }
- \def\fixjfmspacing@{{%
- \@tempcnta=\lastnodechar
- % \@temp の中身を展開し続けた結果の最初は和文文字か?
- \edef\@@temp{\@temp}\expandafter\fixjfmspacing@@\@@temp\relax\@nil%
- %
- \ifnum\@tempcnta>\m@ne
- \setbox0\hbox{\inhibitglue\char\@tempcnta
- \relax\@temp\inhibitglue}%
- \setbox2\hbox{\inhibitglue\char\@tempcnta
- \@temp\inhibitglue}%
- \hskip\dimexpr\wd2-\wd0\relax
- \fi
- }}
- % #1 はもうこれ以上展開不可能なはず
- \def\fixjfmspacing@@#1#2\@nil{%
- \ifcat\fjs@kanji#1\else
- \ifcat\fjs@kana#1\else
- \ifcat\fjs@other#1\else
- \@tempcnta=\m@ne
- \fi
- \fi
- \fi
- }
- \DeclareRobustCommand\mytextgt[1]{%
- \relax\ifmmode\hbox\fi{%
- \gtfamily\fixjfmspacing #1%
- }\fixjfmspacing%
- }
- \makeatother
この \fixjfmspacing 命令を使うと,次のように書体変更命令を間に入れても,文字間には正しく半角空きが入るようになります.
それを内部に使っている \mytextgt 命令を \textgt の代わりに用いると, ひとまず和文文字間の空白がうまくいっている……ように見えます.
- \parindent=0pt
- \def\TEST{『ほ}
- 【例1】\\
- これは,『ほげ党宣言』(ホゲ・フガ著)\\
- これは,\mytextgt{『ほげ党宣言』}(ホゲ・フガ著)\\% OK
- これは,{}\mytextgt{『ほげ党宣言』{}}(ホゲ・フガ著)\\% OK
- これは,\relax\mytextgt{『ほげ党宣言』\relax}(ホゲ・フガ著)\\% OK
- これは,\mytextgt{\TEST げ党宣言』\relax}(ホゲ・フガ著)\\% OK
- これは,\textgt{『ほげ党宣言』}(ホゲ・フガ著)% NG
- 【例2】\\
- これは『ほげ党宣言』という本の……\\
- これは\mytextgt{『ほげ党宣言』}という本の……\\% OK
- これは\textgt{『ほげ党宣言』}という本の……% OK
なお,ここで作成した \fixjfmspacing, \mytextgt 命令は説明のためにある程度単純化して作ったもので, 以下のような注意事項が挙げられます. これらを解決させるのは実際の利用者達に任せることにします^^;
LuaTeX や,その上で動く LuaTeX-ja の場合には,状況が異なります. LuaTeX では,リガチャやカーニングの処理は段落・ボックス終了時に一括してノードベースで行うことになっているので, 従来リガチャの抑止として ff{}i のような方法はもはや使えません.……
また,pTeX 系列では,「異なる和文フォントを使う」ことは,異なるフォントメトリックを用いることを意味していました. 例えば,JIS フォントメトリックには,横組明朝用の jis.tfm と,横組ゴシック用の jisg.tfm という内容が同じ 2 つのフォントメトリックがありました. しかし,LuaTeX-ja では,異なる和文フォントに対しても,同じフォントメトリックを共用することを許しています. 以下の例では,IPAex明朝 (\MC) と IPAexゴシック (\GT) について,同じフォントメトリックを共用しています. LuaTeX の処理がノードベースであることも相まって,「,」と「『」の間には,正しく半角空きが入るようになっています.