Subscribed unsubscribe Subscribe Subscribe

SphinxのTipsを紹介

Sphinx

Sphinxでマニュアルを書き始めてノウハウがちょっとずつたまってきたので、Tipsを書いていこうと思います。整理してまとめて書くと時間がかかり陳腐化しそうなので、書けた部分から小出しで紹介していきます。

includeディレクティブで画像も含めて取り込む方法

Sphinxを採用した理由の1つとして、複数のマニュアルを書く際に共通部分は二重管理したくないという点がありました(参考)。ソフトウェアのいわゆるDRY (Don't repeat yourself) 原則と同じように、ドキュメントでも重複を避けたいというのが大きな理由の1つでした。

Sphinxだと、includeディレクティブを使うことで他のドキュメントを取り込んで再利用することができます。

例)

.. include:: ../common/reusable.rst

課題

includeディレクティブの残念なところとして、figureディレクティブで指定した画像まではうまく扱えないという問題があります。

例えば、Sphinxで作成するドキュメントソースが以下の体系だったとします。ドキュメントがdoc1、doc2の二種類あり、それぞれで共通の文章reusable.rstを含めるようなケースを想定しています。

source
 +-- common
 |    +-- reusable.rst  … chapter1, 2で両方使いたい
 |    +-- sample.png    … reusable.rst内でfigureディレクティブを使い埋め込んでいる画像
 +-- doc1
 |    +-- chapter1.rst  … includeディレクティブでcommon/reusable.rstを含める
 +-- doc2
      +-- chapter1.rst  … includeディレクティブでcommon/reusable.rstを含め

reusable.rstでは、以下のようにfigureディレクティブを使いsample.pngを埋め込んでいるとします。

.. figure:: sample.png
	:scale: 100%
	:width: 500px

これでHTMLを生成しても、残念ながら画像は埋め込まれません。reusable.rstはdoc1、doc2のchapter1.rstにそれぞれマージされてから、figureディレクティブの処理が行われる動きとなっており、doc1、doc2ディレクトリ以下にsample.pngがないと画像が埋め込まれません。

試しに、doc1、doc2ディレクトリ以下にsample.pngを配置してあげるとうまく表示されるようになります。ですがこれだと全くうれしくありません。reusable.rstはDRYにできているのに、画像ファイルは二重管理になってしまいます。

解決策

解決策としては、例えば「ビルドスクリプトで頑張る」のと「Sphinxを拡張する」方法があると思います。

ビルドスクリプトで頑張る

この方式は、単にSphinx実行前に画像ファイルをコピーするという方法です。しかし、再利用元のディレクトリはどこなのかは作成するドキュメント次第なので、ビルドスクリプトのメンテナンスが都度入りそうですしこの方式は避けたいところです。また、同一名の画像ファイルがもしあると単純コピーでは上書きしてしまうので、その考慮も必要になります。

Sphinxを拡張する

もう1つはSphinxを拡張して対応するという方式です。Sphinxではさまざまなレベルで拡張できるように設計がされています(参考:Sphinx拡張)。

今回のケースだと、他にも色々やり方があるかもしれませんが、標準のincludeディレクティブを拡張して対応することができます。

実は、figureディレクティブで画像ファイル名を指定していますが、絶対パスで指定してあげるとincludeディレクティブでもうまく扱えます。例えば、以下のように書きます
*1

.. figure:: //workspace/sample/source/common/sample.png
	:scale: 100%
	:width: 500px

絶対パスで書くことで、includeディレクティブでマージされても画像ファイルにアクセス可能なので大丈夫、というわけです*2

しかし、元からこのような絶対パス方式で書いておくと、ビルド環境が変わった途端に動かなくなるのでオススメできません。また、相対パスでも書くことができるのですが、先ほどの例のように再利用元全てでディレクトリ階層が同じなら問題ないですが、異なると使えません。

というわけで、従来のincludeディレクティブを拡張して、実行時に自動的に絶対パスに展開するという処理を追加してあげれば、うまく対応できそうです。

インストールされているSphinxのオリジナルのincludeディレクティブのソースを直接修正しても対応できますが、ビルド環境依存になってしまうので、今回は拡張モジュールを作成し、Sphinxの拡張モジュールとして登録して対応しました。そうすればソースリポジトリに一緒に格納することができますので、どのSphinx環境でも同じように動かすことが可能になります。

拡張モジュールの設定方法は以下のとおり。

  • source/_extentions/ 以下に拡張モジュールのpyファイルを配置 (例えば、ext_include.pyを配置)
  • conf.pyのextensionsに配置したモジュールを追加
extensions = ['sphinx.ext.todo', 'sphinx.ext.ifconfig', 'ext_include']

ext_include.pyは、docutils-0.9-py2.6のdocutils/parsers/rst/directives/misc.pyをコピーして作成しました。変更点は以下の通りです。追加処理内容としては、.. figure:: を検知して絶対パスに置換しているだけです。
後は、def setup(app)を追加して、ディレクティブとしてSphinxに登録させるようにしています。ディレクティブ名をincludeとし、オリジナルのincludeを上書きしています。

diff --git a/source/_extentions/ext_include.py b/source/_extentions/ext_include.py
index cd371e2..e5fc4cc 100644
--- a/source/_extentions/ext_include.py
+++ b/source/_extentions/ext_include.py
@@ -18,6 +18,10 @@
 from docutils.parsers.rst.roles import set_classes
 from docutils.transforms import misc
 
+def setup(app):
+    app.add_directive('include', Include)
+
+
 class Include(Directive):
 
     """
@@ -60,6 +64,9 @@
         if path.startswith('<') and path.endswith('>'):
             path = os.path.join(self.standard_include_path, path[1:-1])
         path = os.path.normpath(os.path.join(source_dir, path))
+        
+        abspath = path
+        
         path = utils.relative_path(None, path)
         path = nodes.reprunicode(path)
         encoding = self.options.get(
@@ -149,369 +156,14 @@
                                   self.state,
                                   self.state_machine)
             return codeblock.run()
+
+        # convert to abspath
+        for index, line in enumerate(include_lines):
+            if '.. figure:: ' in line:
+                include_lines[index] = re.sub(r':: +', ':: ' + os.path.dirname(abspath).replace('\\', '/') + '/', line)
+                print 'convert figure path to abspath.'
+                print 'Before: ' + line
+                print 'After: ' + include_lines[index]
+
         self.state_machine.insert_input(include_lines, path)
         return []

これで画像ファイルも含めて再利用することができるようになります。

*1:先頭にスラッシュ(/)が2つないとLinuxでは絶対パスとみなされないため、わざとこのように記述しています

*2:絶対パスで定義してもHTML出力時には相対パスに変換されるので大丈夫です