Sphinxで脚注をビルド時に集約する

脚注機能の概要

reStructuredTextの文法には 脚注 というものがある。

この文には、本文に直接的な記述をする必要こそ無いものの、
簡易的な補足情報があるのが望ましい語句が使われている。 [#]_

.. [#] とはいえ、興味がある人のみ参照すれば良いやつ

reST形式のソースで上記のような記述をすると、ドキュメントの構造としては「脚注」と扱われる。 脚注参照先と脚注本体で相互参照が付与され、SphinxのHTMLビルドを行った場合では、 a タグによるリンクも設定される。 [1]

この説明などを見るだけだと単純に便利な機能があるだけなのだが、ドキュメント記述をするとちょっとばかり面倒と感じる点がある。

脚注に関する標準的な挙動

手っ取り早く、上記のソースを docutils に同梱されている rst2pseudoxml.py で構造化してみよう。

<document source="example-footnotes.rst">
  <paragraph>
    この文には、本文に直接的な記述をする必要こそ無いものの、
    簡易的な補足情報があるのが望ましい語句が使われている。
    <footnote_reference auto="1" ids="footnote-reference-1" refid="footnote-1">
      1
  <footnote auto="1" backrefs="footnote-reference-1" ids="footnote-1" names="1">
    <label>
      1
    <paragraph>
        とはいえ、興味がある人のみ参照すれば良いやつ

階層構造としては次のようになっている。

  • 最初の paragraph 要素内に、脚注参照先を示す footnote_reference 要素が定義される。

  • paragraph 要素の直後に、脚注本体を意味する footnote 要素が定義される。

これ自体は自然な挙動だ。

では、次のソースだとどうなるだろうか。

docutilsの拡張はSphinxでも利用できる。 [#]_

.. [#] 逆は必ずしも可能とはいえない。

Sphinx拡張は組み合わせることが可能。 [#]_

.. [#] ただし、設定のバッティングに注意すること。

前提として、「文章に対する脚注は、記述位置が近いほど文としての関連性管理がしやすい」という思想を認識しておいてほしい。

<document source="example-footnotes.rst">
  <paragraph>
    docutilsの拡張はSphinxでも利用できる。
    <footnote_reference auto="1" ids="footnote-reference-1" refid="footnote-1">
      1
  <footnote auto="1" backrefs="footnote-reference-1" ids="footnote-1" names="1">
    <label>
      1
    <paragraph>
      逆は必ずしも可能とはいえない。
  <paragraph>
    Sphinx拡張は組み合わせることが可能。
    <footnote_reference auto="1" ids="footnote-reference-2" refid="footnote-2">
      2
  <footnote auto="1" backrefs="footnote-reference-2" ids="footnote-2" names="2">
    <label>
      2
    <paragraph>
      ただし、設定のバッティングに注意すること。

基本的には最初と同じように解釈され、「 paragraph の直後に footnote が来る」という挙動になる。 もちろん、これも至極まっとうな挙動だろう。 ソースの字句通りに解釈することのほうが重要であるのは間違いない。

しかし、これをSphinxでビルドすると、ちょっと困ったことになる。

Sphinxビルドと脚注

まず前提として、Sphinxのビルドにおけるコンテンツ出力は docutils の振る舞いに準拠している。 よって、ドキュメント構造をルートから順に探索し、 要素の出入り にもとづいて処理が進んでいく。

つまりどうなるかというと、前述のソースをビルドすると 本文の途中で脚注が割り込んで 出力されることになる。 これだと、「脚注」とは言えないだろう。

とは言え、これも docutils + Sphinx の挙動としては何も間違っていない。 更に、この挙動を前提に考えれば、想定されている本来のドキュメント記述としては、 以下のような「脚注は【脚注】らしい位置にいる」状態なのだろう。

docutilsの拡張はSphinxでも利用できる。 [#]_

Sphinx拡張は組み合わせることが可能。 [#]_

.. [#] 逆は必ずしも可能とはいえない。
.. [#] ただし、設定のバッティングに注意すること。

この形式でもドキュメント管理上は正常ではある。 ただし、ドキュメントの分量がある程度多くなった際に、 参照元と参照先の距離が離れると編集がそれだけで大変になるのも事実だろう。

そのため、例えば Zenn 上では思想通りの振る舞いがなされており、 各セクションごとに脚注を書いても、「脚注」として最後に集約されているようになっている。 [3]

そこで、出来ることならSphinxでも同様のことを実現したい。

Sphinx拡張として

プレーンではないことを実現する以上は、Sphinx拡張として実装する必要がある。 出力結果が変われば良いので、基本的には「出力に対する拡張」 [4] と考える…のだが、 HTML出力に割り込んで処理をするのは複雑になりがちなので途中の内部処理で対応するアプローチを取ることにする。

実際のコードはこちら。 一応扱えるように、 Gist にも置いてある。

基本的な考え方をシンプルに説明すると、「 footnote 要素を掻き集めて、新規に用意した空セクション上で纏めて出力する」という手段となっている。

Sphinxのコアイベントの一つに doctree-read という「rstソースをパースして docutils によってdoctreeを生成した」というタイミングが存在する。 この時点で、 doctree オブジェクトはすでに脚注における参照情報まで確定しているため、出力までの間に場所を移動しても、適切に遷移してくれる。

この状態の doctree から .traverse() を用いて footnote 要素を探索し、存在する場合は事前に用意した空の section 要素に移動させている。 最終的に「 sections に子要素がある = footnote が1個以上あった」となるため、この場合に限りこの要素を doctree 直下に挿入して出力対象として扱う。

実際にこのサイトでも動作しており、このページのソースは各セクションに脚注を分散させているが、出力時には正しく末尾に脚注が集約されている。

PyPIに登録するか否か

現時点では、このコードはPyPI上に登録していない。 これは、実装上の課題感として「自分以外が使う際に備えておくべきこと」の想定がまだ足りていないため。

例えば、現時点での実装では「ここから先は集約した脚注」ということを示すために .. rubric:: ※脚注 と同等の処理を section の先頭子要素に追加しているのだが、 この時点で次の課題がある。

  • 日本語以外だと何が記載されているべきか

  • rubric 以外の要素を使ったほうが良いのか

  • そもそも、脚注であることを示す必要を感じてないのではないか

極論を言えば、「この拡張は日本語ドキュメントのみを前提とした実装です」と言い張って公開することも可能ではあるのだが、 このあたりの課題に対する落とし所ぐらいは考えて置きたいというのが本音となっている。

※脚注