Sphinx ではどのようにラベルとキャプションを結びつけているのか

Sphinx ではラベル記法と :ref: 記法を使って、
ドキュメントの様々な位置にラベルを張り、それを参照することができます。

.. _target:

section
---------

セクションへの :ref:`参照 <target>` を作ります。

さて、このとき、ラベルを貼っているのがセクションタイトルなどの場合は
:ref:`target` という書き方でキャプションをリンクタイトルにすることができます。

ラベルとキャプションを認識する

Sphinx は次の順序でラベルを認識します。

  1. reST ファイルのパース処理において、ラベル記法をパターンマッチで見つける (docutils.parsers.rst.states:Body#explicit_construct())
  2. target ノードを生成する (docutils.parsers.rst.states:Body#make_target())
  3. ドキュメントに target ノードの情報を登録する (docutils.nodes:document#note_explicit_target())
  4. std ドメインにてリンクターゲットを処理する (sphinx.domains.std:StandardDomain#process_doc())
    • ドキュメントに登録された target ノードの情報を、ラベルデータとして再登録
    • ラベルを貼ったノードが section, figure, image, table のいずれかのノードであり、キャプションを持っている場合、キャプションをラベルデータとともに記録

Sphinx は docutils と処理が入り交じっていて何が行われているのか把握しづらいのですが、
複数のファイル間でクロスリファレンスを行うために、
docutils がパースしたリンク情報を std ドメインに蓄積、再構築して、
ファイルをまたいでもラベルやキャプションが参照できるようにするという処理になっています。

ここで作成したラベルとキャプションはファイルを超えてリンクを貼る (domain#resolve_xref() のすゝめ) で紹介した
domain#resolve_xref() などで取り出すことができます。
:ref: で参照するときにも同様にドメインを介して、ラベルとキャプションを取り出して、
リンクを組み立てています。

独自のノードを作るときの問題

Sphinx がラベルとキャプションを扱うときの動作を説明してきましたが、
もしキャプションを扱うような Sphinx 拡張を作る場合は上記のことを頭に入れておく必要があります。

ポイントとなるのは ラベルを貼ったノードが section, figure, image, table のいずれかのノードであり という部分で、
独自に作成したノードの場合は :ref: で参照する際にキャプションを取り出すことができません。

そのため、どのような Sphinx 拡張を作るかによってアプローチが変わってきます。

画像(やテーブル)を扱う Sphinx 拡張を作る場合は…

graphviz や PlantUML, blockdiag などのように、画像を生成する系列の拡張の場合は、
通常のアプローチでは次のような実装をすることが多いと思います。

  • Directive では独自ノードを作り
  • visitor 関数で独自ノードを画像に変換する

しかし、キャプションをうまく扱うためには次のようなやり方をするとよいでしょう。

  • Directive では figure ノード、caption ノードと画像を表す独自ノードを作る
  • visitor 関数で独自ノードを画像に変換する

こうすることで、Sphinxドメインにキャプションを認識させつつ、
独自ノードの部分は自分の visitor 関数で処理することができます。
このやり方は sphinx.ext.graphviz の実装が参考になります(figure_wrapper() のあたり)。

ここでは画像について触れましたが、テーブル系に関しては
table ノード、title ノードを使ってテーブルの外郭部分を構築するとよいでしょう。

それ以外のノードの場合は…

それ以外のノードに関しては、現時点ではラベルとキャプションを紐付けることはできません。
:ref: 記法を使ってキャプションを取り出すことは諦めることになります。
回避方法としては、:ref: に変わる別のロールを設けるやり方があります。
new_numfig.py の実装(:ref:num: ロールを作った)が参考になるかもしれません。

なお、コミッターの @shimizukawa さんもその部分は認識しているようなので、
リクエストを上げ続けるといずれ改善されるかもしれません。(C'mon PR!!)


話は変わって LaTeX の話

コードを読み進めていくと、LaTeX においてもラベルは少し特殊な扱いになっています。

sphinx.writers.latex:LaTeXTranslator#visit_target() を見ると

  • 次のノードが section, figure, table の場合は各ノードの visitor 関数でラベル作成を行う
  • それ以外のノードの場合は \phantomsection を使って、その場にダミーのリンクターゲットを作って、そこにラベルを作成する

という手順を踏んでいます。

生成された PDF を見ると、リンク先の位置がちょっと変わることになります。
ここも画像やテーブル系の拡張であれば、先ほど紹介したやり方で回避するとよいでしょう。

まとめ

Sphinx が内部でどのようにクロスリファレンスを実現しているのかを把握すると、
独自の Sphinx 拡張を作るときに役に立ちます。
フローが複雑でコードが追いづらい箇所ではありますが、
キャプションを扱う拡張を書く場合は目を通しておくと使い勝手が上がると思います。

ちなみに、このまとめは sphinx.ext.graphviz のキャプションが :ref: で拾えなかったのを直す際に調べました。