ドメインの処理の流れ

date:2013/01/01
last modified:2013/01/09
author:渋川よしき
  1. ドメインの登録
    • ドメイン用のロールとディレクティブを作成
    • ロールとディレクティブの登録
  2. ドキュメントのパース時
    • ディレクティブの引数を分析
    • ロールの実装
    • リンクを解決
    • インデックスページを作成

ドメイン開発で参考にするもの

基本的には既存のドメインを参考にして改造して作っていくことになるでしょう。作りたい言語に近い特性を持つ言語を選びましょう。例えば、クラスとモジュールがあればPythonやRuby、関数型言語(アリティがある)ならErlang、といった具合です。本当に近いものが見つかれば、90%終わったも同然です。

ロールとディレクティブの作成

まずはディレクティブを作成します。ユーザの学習コストを下げるために、Pythonドメインと同じにできるところは同じにしましょう。ディレクティブは「引数のタイプ」と、「階層上の位置」の種類だけ作ります。

例えば、Rubyの場合、クラスメソッドとアトリビュートはクラスのメンバーにしかなりません。グローバル変数はグローバルに属します。定数とメソッドはクラスのメンバーにもできるし、モジュールのメンバーにもなります。関数はモジュールに属しますよね。言語を知っていると、メソッドとクラスメソッドと関数を同じディレクティブにしたくなるところですが、ぐっとこらえましょう。

とはいえ、共通化できるところも多いので、Rubyの場合は次のようなクラス階層になっています。多くのコードがRubyObjectにあります。これは説明を書くためのディレクティブです。RubyModuleRubyCurrentModuleはPythonドメインに準拠しています。インデックスでモジュール名をクリックしたときにはRubyModuleディレクティブの場所にジャンプします。RubyCurrentModuleはアンカーは作成しないが、そのディレクティブから先に置かれたクラスなどを特定のモジュールに属させたい場合に利用します。どちらも、説明を書くディレクティブではなく、フラグのようなディレクティブなので、多機能なObjectDescriptionではなく、Directiveを直接利用します。

- sphinx.directives.ObjectDescription
   + RubyObject
     + RubyGloballevel
     + RubyModulelevel
     + RubyEverywhere
     + RubyClasslike
     + RubyClassmember

- sphinx.util.compat.Directive
   + RubyModule
   + RubyCurrentModule

ロールは基本的に1種類で済むでしょう。ただし、引数の数やカッコの有無など、自動で修正したい場合はそのような機能を実装します。

ロールとディレクティブの登録

Domainクラスを継承したクラスを作ります。object_types,directives,roles,initial_data,indicesというクラス変数に設定を登録していきます。534行目のクラス定義を参照してください。

directivesは、実際にドキュメントで使うディレクティブ名と、先程作ったディレクティブクラスの対応表です。rolesも、実際にドキュメントで使うロール名と、ロールオブジェクトの対応表です。object_typesは、ディレクティブとロールを結びつける情報です。それ以外については後述します。

クラスができたら、 688行目のsetup関数と同じコードを書いて、拡張機能が登録されたときにドメインを追加するようにします。

ディレクティブの引数の分析

51行目からのRubyObjectクラスの中で引数の分析を行なっています。

ディレクティブのオプションは、doc_field_typesクラス変数で設定します。これを登録するだけで、関数やメソッドなどの属性の説明が行えるようになります。

ディレクティブに関する処理は89行目からのhandle_signatureメソッドで行なっています。

このメソッドでやっていることは、主に以下のタスクです。

  • 正規表現を使ってディレクティブの引数の分析(98行目〜)

    長い正規表現になりがちなので、re.VERBOSEを利用すると良いでしょう。

  • 完全修飾名を作る(105行目〜)

    SphinxのPythonドメインでは、メソッド説明を書く時に、.. py:method:: ClassName#methodNameという名前でも書けますし、.. py:class:: ClassNameディレクティブを書いて、その中に.. py:method:: methodNameと書くこともできます。どちらの場合も同じロール名で参照できる必要があるため、文脈情報を利用して、完全修飾名を作ります。self.env.temp_dataでソースを検索してみてください。登録と情報取得はそれほど難しくはありません。

    ディレクティブの完全修飾名を作るのに、ディレクティブのネスト情報を参照する必要があるかもしれません。ディレクティブはネストされたとき、出るときにbefore_content()after_content()が呼ばれるので、この中でスタックを自前で記録していけばさまざまなことができます。直前の親の型がルールに従ってないとNGなどもできると思います。namesに関してはこのセクションの最後で説明しています。

    def before_content(self):
        if self.names:
            self.env.temp_data.setdefault('rb:directivenest', []).append(self.names[0][0])
    
    def after_content(self):
        if 'db:columnfamily' in self.env.temp_data and self.names:
            self.env.temp_data['rb:directivenest'].pop()
    
  • タグを追加して、情報を整形する(135行目〜)

    Sphinxのaddnodesモジュールの関数を利用して、ドキュメントを整形します。返り値を最初に書きたい、後に書きたいなどはここを調整することで変更することができます。

  • 最後に、完全修飾名と、明示的に付与されたプリフィックス(なければ空文字でOK)をreturn返す

    例えば、.. py:function:: modulename.functionと書かれれば、'modulename.'がプリフィックスです。ここで返した値はself.namesに格納されるので、あとで参照できます。

なお、self.contents, self.optionsにオプションの値が入っています。

ロールの実装

ロールの実装に関してはあまり多くのことをする必要はありません。区切り文字の正規化と、チルダを使った記法のサポートぐらいです。440行目のRubyXRefRoleクラスで実装されています。

リンクを解決

ロールで名前を指定したときに、適切なディレクティブの場所にリンクをするために、アンカー情報を登録します。195行目のadd_target_and_indexメソッドでこの処理を行なっています。ここではself.env.domaindata['rb']['objects']にすべての名前を登録しています。なお、Rubyの場合は['objects']にすべて格納していますが、例えば関数がファーストクラスではなく、変数と関数で同名のものが利用できるのであれば、別の辞書に格納させます。

またここでは、インデックスの作成も行なっています。

リンクの解決は594行目のfind_objメソッドで行なっています。完全修飾名を類推して返します。Rubyではメソッド名の区切りに#を使ったり、::をつかったり、.を使ったりしますが、そのようなファジーな検索はここで行います。

モジュールに関するディレクティブも、self.env.domaindata['rb']['modules']にモジュール情報を登録しています。このdomaindataにプログラム言語中の名前空間と同じオブジェクト階層を作るのが、ドメイン実装のキモです。

インデックスを作成

Sphinxの期待する形式でインデックス情報を作成して返します。474行目のgenerateテンプレートメソッドで実装されています。

仕上げ

SphinxのドメインのAPIで指定されているメソッド(resolve_xref,get_objectsなど)をいくつか追加します。ほぼ、参照元のコードのコピーでいけると思います。後は動くようになるまでデバッグして完成させます。拡張開発最初の一歩で紹介したように、最初に受け入れテスト的なドキュメントを作成しておくことをおすすめします。クロスリファレンスが適切に解決できるかどうかがポイントです。

Sphinxのドメインの場合、完全修飾名、モジュールやクラスの省略を考えると、どうしても組み合わせが複雑になってくるので、ユーザに読ませるドキュメントと、テスト用のドキュメントは別にした方が良いでしょう。Rubyの場合は次の組み合わせが考えられます。

  • モジュール外から
    • 他のモジュール内のクラスを参照
    • 他のモジュール内のクラスのメソッドを参照
    • 他のモジュール内のクラスの属性を参照
    • 他のモジュール内の関数を参照
    • グローバル要素の参照
  • モジュールの中から
    • 完全修飾名でモジュール内のクラスを参照
    • クラス名だけでモジュール内のクラスを参照。
    • 完全修飾名でモジュール内の関数を参照
    • クラス名だけでモジュール内の関数を参照。
  • クラス内から
    • 完全修飾名でメソッドを参照
    • メソッド名だけでメソッドを参照
    • 完全修飾名で属性を参照
    • 属性名だけで属性を参照

それ以外にも、モジュールのネストなどもあります。

Rubyドメインが行なっているテストはここにあります。