📜

Sphinxのtoctreeにおける順序操作

2021/01/12に公開

誰向けか

  • Sphinxでドキュメンテーションなどを行っている
  • toctreeで全部のドキュメントをツリーにしている
  • そのtoctreeglobを使っている
  • このときに、Sphinx内部処理とは別のルールでソートさせたい

何が困っているのか

自分が個人で管理しているサイトはSphinx製で、今時点では ablog というブログ用のSphinx拡張を利用してブログを書いています。

ablog単体でできないことがある

ablog ではブログ設定で指定した配下のreSTを記事として扱うのですが、標準的な使い方の案内としてtoctreeを用いないものとなっており、以下のようなちょっとした弊害が発生します。

  • next,prevといったlinkタグ用のリレーションが生成されない [1]
  • ソースreSTが独立してしまっており、:doc:ロールなどを使ったリンクが生成できない

これらの機能は、Sphinx内で各ドキュメントtoctreeの観測範囲に収まっていることが前提になっています。そのため、:glob:フラグをオンにしたtoctreeディレクティブを用意してあげることで解消できます。

blog.rst

.. toctree::
   :glob:
   
   blog/**/*

このドキュメントをマスタードキュメント経由でtoctreeに収めることができれば、全記事のリレーションが貼られるため、next/prevの用意や記事間リンクが機能するようになります。

toctreeの挙動で困ったこと

しかし、何も考えずにglobで全記事を収めると、「Sphinxとしての記事の前後関係」と「ablogとしての記事の前後関係」の不一致を起こします。

具体的には...

title-a.rst

.. post:: 2021-01-11
title-b.rst

.. post:: 2021-01-01
title-c.rst

.. post:: 2021-01-10

このようなソースがあった場合、ablogとしては日付をもとに前後関係を判断するためtitle-a->title-c->title-bの通りにナビゲート情報が用意されるのに対して、Sphinxのtoctree with globでは内部のファイル探索に基づくため title-a->title-b->title-cの順になってしまいます。[2]

このままではあまりよろしくないので、ちょっと対策を考えてました。

どう解決するか

幸いSphinxにはイベントハンドラを差し込める受け口が用意されています。
そのため、「ソース読み込み完了」と「HTML出力」までの間にtoctreeの中身をいじることでどうにかしてみることにします。

00. 前提として

以下の前提で話を進めます

  • index.rstには、blog.rstへのtoctree参照がある
  • blog.rstには、フォルダは以下すべてを含むblog/**/*へのtoctree参照がある

0. コード

conf.py
blog_path = "blog"

def resolve_posts_order(app, env):
    from sphinx.addnodes import toctree
    blog_path = app.config.blog_path
    blog_top_doctree = env.get_doctree(blog_path)
    toctree_node: toctree = list(blog_top_doctree.traverse(toctree))[0]
    entries = []
    for _, docname in toctree_node["entries"]:
        postinfo = env.ablog_posts[docname][0]
        entries.append((_, docname, postinfo))
    entries = sorted(entries, reverse=True, key=lambda e: e[2]['date'])
    toctree_node["entries"] = [(e[0], e[1]) for e in entries]
    toctree_node["includefiles"] = [e[1] for e in entries]
    env.toctree_includes[blog_path] = toctree_node["includefiles"]

def setup(app):
    app.connect("env-updated", resolve_posts_order)

1. どのタイミングでならうまくいくかを探す

書き換えのためには、ブログ記事のメタデータが全て出揃っている必要があります。

ablogの挙動は、各ブログ記事をdoctreeとして読み込んだ時点で、メタデータを抽出して中央管理している記事情報に引き渡す仕様です。
そのため、blog.rstの読み込みだけでなく、全てのソースを一通り読み込み終えた状態が理想です。
Sphinxのコアイベント的にはenv-updated,env-get-updated,env-check-consistencyあたりならHTML書き出し前なので無難そうです。
ここでは、env-updatedのタイミングで処理をすることにします。

2. blog.rst内のtoctree順序を書き換える

前述の通り、この時点でのtoctree内の記事リストは、glob処理に基づいて決定されています。
したがって(おそらく)ファイルの文字列順でリストが生成されています。
これを、ブログ記事の日付に基づいた順序に変更してやります。
resolve_posts_orderの中身の殆どの処理がそれ

  1. 処理対象のdoctreeをピックアップ
  2. その中からtoctreeノードをピックアップ
  3. tocreeノード内にある記事情報をもとに全記事情報から日付情報をピックアップ
  4. 日付情報をもとに記事一覧をソートして、toctreeに書き戻し

といったような流れになっています。
内部で大量に呼んでいるget_doctreeファイルI/Oがあるので、ある程度記事数が増えるとちょっと処理時間に影響があるかもしれません。

リンク

脚注
  1. ablog固有機能として、前後ナビゲーション自体は生成されます ↩︎

  2. 当然ですが、どちらもそれぞれの観点で見れば当然の振る舞いです。 ↩︎

Discussion