golden-luckyの日記

ツイッターより長くなるやつ

TeXの脚注をなんとかする

この記事はTeX & LaTeX Advent Calendar 2019の24日目の記事です。23日めはwtsnさんの記事でした。25日めは☃さんの記事です。

今年は3年ぶりにTUGに参加してきました。 TUGというのはTeX User Groupのことであり、TeX界隈の開発者とアドバンストな利用者からなる世界的なコミュニティです。 年に1回、地球のどこかで集まっていて、それもまたTUGと呼ばれています。 2013年には東京でも開かれました。

なお、今年はKnuthも参加しています。前回の参加は10年くらい前のことで、そのときはiTeXというネタ発表をしたことでも話題になりました。 今年は完全に聴衆として参加していたのですが、それでも圧倒的な存在感が圧倒的でした。

日本からはわたしを含めて5人が参加し、それぞれ思い思いのKnuth体験をしたようです。 わたしは圧倒的に圧倒されて距離をとっていたのですが、気が付いたら寺田さんたちが一緒に写真を撮っており、それに混ざっていたら、ちょっとだけお話をする機会が得られました。

実は、以前からKnuth本人に直接尋ねてみたかったことがあったのです。 それは、「Knuthの奥様は本当に脚注が嫌いなんだろうか?」ということでした。 というのも、"The TeXbook"の15章の末尾には次のような引用があって、これがずっと気になっていたからです。

Don't use footnotes in your books, Don.
--- JILL KNUTH (1962)

わたしは、一読者として脚注が好きです。 脚注が多い本はよくない、という一般論は理解しているのですが、これまで自分が本を読みながら、その内容を理解するにあたっては、脚注の存在に何度も助けられてきました。 リニアに読んで理解できるように編集されているのがベストというのは、ほんとその通りだと思います。 が、そもそも世の中はリニアに説明しやすいことばかりではなく、それで脚注をなくすことに腐心するくらいなら、むしろ脚注を濫用するくらいのほうがまし、とさえ思うのです。 本文で説明を完結させるのが前提として、すっきり書き上げた本文に脚注が追加されて解説に厚みが出ることは、読者としては歓迎すべきことではないだろうかと。

で、せっかくなのでKnuthに「奥様はほんとに脚注が嫌いだったの?」と質問してみました。 いま考えると唐突な質問でしたが、すぐに15章の引用のことを思い出してもらえて、「あの引用は本当に彼女が言ったことなんだ。けど、もちろん冗談としてね」と教えてもらえました。 まあ、おおむね予想の範囲内の回答だったといえますが、冗談であるという言質を得ることはできて安心しました。

さらに、「脚注をTeXに実装したということは、自分でも脚注を使うつもりはあったんですよね?」とも聞いてみました。 すると、「いままで4000ページくらい書いてきて、4つだけ脚注を使ったよ」と自慢されました。 どの本で使ったのかは聞き損ねたのですが、日本に帰ってから持っている本をぱらぱら見たところ、TAOCP 2edのVol.2に1つだけ見つけました。 ほかにKnuthの脚注を見つけた人がいたら教えてください(TeXブックで脚注の例として紹介されてるやつらは除く)。

TeXの脚注は出力されない場合がありますね

さて、その脚注なんですが、TeXには確かに脚注のための仕組みが実装されています。 しかし、この仕組みではあらゆるところに脚注を配置できません。 典型的に問題になるのは次のようなケースです。

\vbox{abc\footnote*{def}.} % この脚注は消える
\bye

LaTeXでも同様です。

\documentclass{article}
\begin{document}
\vbox{abc\footnote{def}.} % この脚注は消える
\end{document}

消えないまでも、脚注として期待される場所、つまりページの最下部に脚注が出ないという制限がある場合もあります。 「 minipage で箱の最下部に出てしまう脚注をページの最下部に出したい」というのはLaTeX初心者あるあるですね。 最近だと、 tcolorbox で同様の問題に苦労している人もいると思います。 対症療法として「\footnotemark\footnotetext を使う」というテクニックを知っている人は少なくないと思いますが、そもそもなんでこんな仕様になっているのでしょうか?

TeXはなぜ脚注を期待どおりに扱えないのだろう

TeXの脚注がいろいろアレなのは、TeXがページ組み立ての際に脚注を「特別な行」として扱うことが根本的な原因です。 大事なところなので、少しだけだけ詳しく説明します。

TeXは本文を組むとき、まず段落をいい感じの見た目になるように行分割します。 それから、各行を「メイン垂直リスト」(MVL)という場所にいったん集めます。 このMVLに行を集める処理のことを「ページビルダ」と呼びます。

いま、ある行に \footnote{...} が出てきたとしましょう。 ページビルダは、その脚注の中身を「特別な行」とみなし、MVLでは該当する行の直下に追加します。

f:id:golden-lucky:20191223231657p:plain

ちょっと紛らわしいのですが、ページビルダは最終的なページを作るわけではありません。 最終的なページは、ページビルダがMVLに集めた行からさらに1ページ分の材料を配置することにより作り上げられます。 この作り上げられた最終的なページは \box255 という箱に入れられます。 また、その際に「特別な行」は「insert」という特別な箱に入れられます。 もともと脚注だったやつであれば、この特別な箱は \box\footins です。 最後にこれらの箱をDVIなりPDFなりの1ページとして出力するのが「出力ルーチン」(OTR)という仕組みです。

f:id:golden-lucky:20191223231712p:plain

ここでポイントになるのは、 \vbox の中に \footnote が出てきてもページビルダがそれを「特別な行」として扱えない、という事実です。 それどころか、ページビルダは \vbox の中を覗きません。 つまり、そもそも脚注があることに気付かないのです。 というわけで、 \vbox の中に出てくる \footnote は、かなり早い段階でTeXからは「不可視」の存在になっているのです。

脚注をMVL上で可視にすればよい

原因が見えれば対処の方法も考えられるというものです。 具体的には、不可視の脚注をページビルダから見えるようにしてMVL上に載せる方法を考えます。

とはいえ、ページビルダはどうやっても \vbox の中は見ません。 なんとかして \footnote の中身を \vbox の外に出して上げる必要があります。

それを手動でやる方法が \footnotetext テクニックです。 このテクニックで実際にやっていることは、「その \vbox が配置されるページ」と同じページの材料としてMVLに載る場所(つまり本文のどこか)を人間が判断し、そこに脚注の中身を置く、という手作業だったわけです。

同じことを自動的にやる方法もあります。 いったん \vbox の中身をトークンリストという場所に逃がしておき、それを \vbox の直下で復元する、というマクロを設置するのです。 具体的にはこういう仕掛けを書きます。

\newtoks\mftn
\def\mfootnote#1{%
  \footnotemark
  \edef\@tempa{\the\mftn\noexpand\footnotetext[\the\c@footnote]}%
  \global\mftn\expandafter{\@tempa{#1}}}%
\def\mfootnoteout{%
  \the\mftn
  \global\mftn{}}

\begin{...}
  \let\footnote\mfootnote
  ...
\end{...}
\mfootnoteout

ページ分割する箱にも脚注を入れたい

トークンリストによる方法は、tcolorboxminipage など、 \vbox と同等な他の箱の中に脚注を配置するときの手法としてもだいたいうまくいきます。 しかし、breakable オプション付きの tcolorboxframed などで、箱の途中にページ分割が起きる場合にはうまくいきません。 箱が複数のページに分かれてしまうので、「その箱が配置されるページ」と同じページの材料としてMVLに載せる、という戦略がそもそも成り立たないからです。

筆者は昨年のTeX & LaTeXアドベントカレンダーにおいて、この問題に対する解決のアプローチがLuaTeXで得られることを示しました。

ただ、昨年の時点では脚注の高さを計算する部分の作りこみが甘く、ページの高さが紙面のサイズを越えてしまう場合がかなりありました。 今年はそれを作り込んだので、それをTeX Conf 2019で発表することを予定していました。 また、昨年のアドベントカレンダーでは時間切れになって説明をまったく書かなかったので、今日の記事で書いたような話をひととおり話すつもりでいました。

以下の図は改良版の yafootnote による tcolorbox への脚注の実例です。入れ子を含むかなり複雑な状況にも対応できているのがわかると思います(ちなみに入れ子の内側を breakable にすることは tcolorbox の制限でできないはず)。

f:id:golden-lucky:20191224000256p:plain

yafootnoteの実装について(読まなくてもよい)

というわけで、最後にこの yafootnote が何をしているのかを雑に説明しておきます。ほとんど開発メモです。

yafootnote は、LuaTeXのいくつかのコールバックを利用することで「ページビルダがやっているTeXの脚注のメカニズムをMVL以外でも模倣する」ものです。 具体的には、下記のように各種のコールバックを利用します。

  • post_linebreak_filter コールバックを利用して、「脚注の中身を特別な行として該当する行の下に移動する」
  • vpack_filter コールバックを利用して、「MVLに載らない特別な行の高さをゼロにする」
  • buildpage_filter コールバックを利用して、 「tcolorbox で分割される場合にページの高さを必要なだけ減らす」
  • pre_output_filter コールバックを利用して、「MVLの特別な行を \box\footins に移動する」

post_linebreak_filter コールバックで脚注を「特別な行」としてMVLに載せる際には、LuaTeXの「属性」の仕組みを使っています。 TeXマクロで脚注のコマンドを定義するとき、脚注の中身を個別のボックスに入れて、そのボックスに対して特別な属性を設定するという使い方です。 この属性を他のコールバックでも引っ掛けることで、ページビルダに任せずに脚注の材料を \box\footins に移動するようにしています。

buildpage_filter コールバックの使い道がちょっとわかりにくいんですが、これは、 tcolorbox が「ボックスの材料を \vsplit する際の高さ」を計算する際に、それを「脚注の高さ」分だけ減らすという処理に利用しています。 これを計算するためには tcolorbox/tcbbreakable.code.tex の一部にも改造が必要でした。 この「脚注の高さ」は、 tcolorbox による分割ではない通常のページ分割でも考慮する必要があり、それは出力ルーチンでやるしかないので、出力ルーチンにも改造が必要になります。

さらに、ページビルダの動作には、TeXの処理のさまざまなタイミングで非同期的に発生するという罠があります。 そのため、 tcolorbox による分割まで考慮した「脚注の高さ」がbuildpage_filter コールバック経由で計算され尽くすのは、実際にはページが完成した後になってしまいます。 これはいかんともしようがないので、実際の組版ではLuaTeXを2回実行することにしました。 1回めの実行では、各ページで最終的に必要になる「脚注の高さ」をすべて計算し、それを外部ファイルにいったん記録します。 そして2回めの実行で、その情報を使って実際のページを組み立てます。

なお、この手法はFrank MittelbachによるLuaTeXによるフロートの最適配置の研究からヒントを得ました。

まとめ

脚注大好き。