\lastnodechar プリミティブについてこれは TeX & LaTeX Advent Calendar 2014 の 15 日目の記事として作成したものです. 昨日(12/14)は hak7a3 さん,明日(12/16)は CardinalXaro さんです. TeX ユーザの集い 2014 の休憩中, 次のように pLaTeX で記述すると,全角カンマと開き括弧の間には全角空きが挿入されてしまうという挙動が話題になりました. ![]() この記事では,上記の挙動や,解決の助けになることを期待して実装した \lastnodechar プリミティブ について解説します. 関連ページとして,
を挙げておきます. \lastnodechar プリミティブが利用可能な環境\lastnodechar プリミティブは, TeX Live 開発版のソースに r35619 (2014/11/19) にコミットされたものが「まともに使えるはず」のものです. 従って,
また,\lastnodechar プリミティブは, e-拡張が有効になっている e-pTeX, e-upTeX にのみ実装されています. コマンド名で言えば,
pTeX での和文メトリック由来のグルーさて,pTeX 系列(ASCII pTeX, upTeX, e-pTeX, e-upTeX)では,和文文字間に入る空白について,次のような仕様になっています.
それぞれの後半部の「」の量は,フォントメトリックによって決められています.和文フォントメトリック(pTeX 公式ページ内のファイルフォーマットの解説)では,和文文字をいくつかの文字クラスにグループ化し,文字クラスごとに寸法・空白の指定をすることになっています. 上記の空白挿入の仕様は欧文におけるリガチャ・カーニング処理に類似した仕様になっており, 誤解を恐れずに言えば「和文文字の連続が『単語』をなし,単語の前後には文字クラス 0 の和文文字が仮想的にあるものとみなす」となります. 基本的な例上の説明では何を述べているかわかりづらいと思うので,具体例を述べます. 例えば,pLaTeX2e 新ドキュメントクラス などで使われる JIS フォントメトリックでは,次のようになっています.
このとき,次の入力を考えてみましょう. すると,次のようになります.
「直後」の意味上の説明には 2 点補足があります.
特に今の場合重要になるのは前者で,例えば次の1行目のように入力すると, 「,」「『」の間には合計で全角空きが挿入されることになります. ![]() なぜなら以下のように,「,」「『」それぞれに由来する半角空きが挿入されるからです.
マクロを使った例も載せてみます. 上の場合,ソース中で「,」の次にあるのは制御綴 \hoge ですが, それは「『」へと展開されます.結果として,「,」の直後の展開不能トークンは「『」ということとなり,両者の間には 半角空きが入ることとなります. pTeX 系列での「段落開始時の開き括弧が全角二分下がりになる」のもこれで説明がつきます. ![]() 最初の「『」の直前は和文文字ではないので, 1. のケースとなり,「『」の直前には半角空きが入る,というわけです. 本記事冒頭の解説以上を元に,本記事冒頭の例について見ていくことにします. 例えば新ドキュメントクラスを利用している場合に,\textgt の定義を \show\textgt によって調べると次のようになります. > \textgt=macro: #1->\relax \ifmmode \hbox \fi {\gtfamily #1}.言い換えれば,本記事冒頭の例は,次のように書いたのとほぼ同じことになります. よって次の2点がわかります.
結果として「,」由来の半角空き,「『」由来の半角空きの両方が「,」と「『」の間に挿入される,というわけです. pLaTeX 標準クラス jarticle.cls などでは \textgt の定義が異なりますが,状況としては同様で, 「,」由来・「『」由来の両方の空きが「,」と「『」の間に挿入されることになります. ここまでが前振りです. \lastnodechar の必要性「望ましくない全角空きが入る」問題解消のために考えられる方法として,和文間のグルー挿入を抑止する \inhibitglue を挿入するというのがあります. 例えば以下のコードでは,「『」由来の半角空き・「,」由来の半角空きの挿入を抑止することで,両者の間が半角空きになるようにしています. ![]() しかし,自動で一律に \inhibitglue を入れるのは望ましくありません. 以下の例では,入ってほしいはずの半角空きがなくなります. ![]() というわけで,空きが正しくなるような「\textgt 改」とでも言うべき命令を作るためには, 命令の直前の文字と,命令の引数の最初の和文文字を読み取り,それに応じて処理を分岐させる必要が出てきます. 後者は \futurelet などの利用でまあなんとかなりますが, 前者の「命令の直前の文字を知る」方法を(少なくとも私は)知りません.たぶん不可能だったのでしょう. それを言うためには,TeX by Topic の 第 1.1 節などに説明がある,TeX の 4 つの処理段階を述べる必要があります.
重要なのは,一旦「胃」で実行された後は「口」には戻ってこれないということです. 例えば,本記事の最初から出している例で\textgt を展開しよう,という段になってみると, その直前の「,」は既に実行が終わっている段階なので,\textgt の中で直前の「,」を(トークンとして)知ることは出来ない, というわけです. 別の言葉で述べますと,TeX では文字・空白,罫線などは最終的にノードという形になり, 段落やページの中身はノード達のリンクリストとして表現されることになります. 第 3 段階の「胃」はトークンをノードに変換する過程であり,実行が終わって 一度ノードに変換されたものはトークンに戻せないわけです. これが,私が \lastnodechar プリミティブを必要だと考えたわけになります. 「直前の文字」をトークンとして知ることが出来ないにしても,ノードに変換されているはずだから, そこから「直前の文字」についての情報を得ることはできるはずだ,ということです. \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 の文字』との間に空白が入る」ことが フォントメトリックによって決まっている場合があります,本記事前半の説明から,\lastnodechar を用いて何かをする直前に そのような文字が来た場合は,\lastnodechar 実行時における「最後のノード」は 大抵空白を表すグルー(またはカーン)となります. e-拡張で定義されている「最後のノード」の種類を返す \lastnodetype というプリミティブでは,その空白を拾います. しかし,\lastnodechar は実装目的からしてこのような動作は望ましくないので, このようなメトリック由来の空白を \lastnodechar は無視します.言い換えれば, 以下のソースは期待されたとおりに「』」の内部コードを返す,というわけです. 禁則用ペナルティ同様に,\lastnodechar 直前が和文文字で,かつその和文文字における \postbreakpenalty の値が 0 でない場合は, ペナルティが最後のノードになります. \hbox(0.0+0.0)x9.24687 \JY1/mc/m/n/10 あ \JY1/mc/m/n/10 っ \penalty 500(for kinsoku) 場合によっては,メトリック由来の空白も同時に挿入されることもありえますが, その場合は空白の方が後に来ることになります. \hbox(0.0+0.0)x9.24687 \JY1/mc/m/n/10 例 \JY1/mc/m/n/10 ) \penalty -1(for kinsoku) \glue(refer from jfm) 4.62343 minus 4.62343 禁則用ペナルティについても,\lastnodechar は禁則用ペナルティを無視 するようになっています. \lastnodechar の応用例例として,次で定義される \fixjfmspacing, \mytextgt 命令を考えてみます.
この \fixjfmspacing 命令を使うと,次のように書体変更命令を間に入れても,文字間には正しく半角空きが入るようになります. それを内部に使っている \mytextgt 命令を \textgt の代わりに用いると, ひとまず和文文字間の空白がうまくいっている……ように見えます.
![]() なお,ここで作成した \fixjfmspacing, \mytextgt 命令は説明のためにある程度単純化して作ったもので, 以下のような注意事項が挙げられます.これらを解決させるのは実際の利用者達に任せることにします^^;
番外:LuaTeX-ja の場合LuaTeX や,その上で動く LuaTeX-ja の場合には,状況が異なります. LuaTeX におけるリガチャ・カーニングLuaTeX では,リガチャやカーニングの処理は段落・ボックス終了時に一括してノードベースで行うことになっています. 例えば,段落途中でノードの並びを表示させてみると \whatsit .\localinterlinepenalty=0 .\localbrokenpenalty=0 .\localleftbox=null .\localrightbox=null \hbox(0.0+0.0)x9.24866, direction TLT \OT1/cmr/m/n/10 f \OT1/cmr/m/n/10 f \OT1/cmr/m/n/10 iとなっており,\par などによって段落が終了したときに初めて,この f, f, i の 3 ノードがリガチャになるなけです. そのため,従来リガチャの抑止として用いられた ff{}i のような方法はもはや使えません.空グループ {} はノードを生成しないからです. 以上のことはカーニングに対してもあてはまります. LuaTeX-ja での和文フォントpTeX 系列では,「異なる和文フォントを使う」ことは,異なるフォントメトリックを用いることを意味していました. 例えば,JIS フォントメトリック中にある横組明朝用の jis.tfm と横組ゴシック用の jisg.tfm は,内容は同じですが 異なるフォントメトリックです. 一方,LuaTeX-ja では,異なる和文フォントに対しても,同じフォントメトリックを共用することを許しています. 以下の例では,IPAex明朝 (\MC) と IPAexゴシック (\GT) について,同じメトリックを共用しています. ノード的に言えば,\MC の「,」の直後に \GT の「『」が続いている*4わけですが,両者は同じメトリックなので, 両者の間には(「,」と「『」の間に入るべき)半角空きが入るようになっています.
LuaTeX における \lastnodechar 代替物LuaTeX で \lastnodechar 代替物を作ることは比較的簡単です. 本来の e-pTeX の \lastnodechar と異なる点として,これは返り値を文字列 で返すことが挙げられます. また,LuaTeX-ja では,内部処理用に様々な(whatsit と呼ばれる)ノード達を使っているので,上のような単純な定義では 次のような場合に対応できません. |