Docutilsのディレクティブを自作する
現在、Djangoの勉強を兼ねて簡単なCMSを作成しています。CMSに必要な機能の1つに、Wiki記法やはてな記法のような軽量マークアップ言語のサポートがあります。DjangoではデフォルトでTextile、Markdown、reStructuredTextの3種類のマークアップ言語が利用可能です。しかし、せっかく自分のCMSを作るので、マークアップ言語も自分仕様にしたいものです。かといって、パーサーを一から自分で作るのは大変なので、ここではDocutilsを拡張するというアプローチを取ることにします。
DocutilsはPyhtonで書かれたテキスト処理用のツール群及びライブラリで、reStructuredTextという形式で書かれたテキストをHTMLやTeXに変換することができます。Docutils及びreStructuredTextについては以下のサイトを参考にしてください。
- http://docutils.sourceforge.net/
- http://docutils.sourceforge.net/rst.html
- http://www.planewave.org/translations/rst/quickref.html
reStructuredTextにはディレクティブという汎用の拡張機能があり、一つの記法で様々な機能をサポートすることができます。ディレクティブは以下のシンタックスに従います。
+-------+-------------------------------+ | ".. " | directive type "::" directive | +-------+ block | | | +-------------------------------+
例えば、imageディレクティブの場合、以下のように使用します。
.. image:: picture.jpeg :height: 100px :width: 200 px :scale: 50 % :alt: alternate text :align: right
また、ディレクティブをインライン要素として使いたい場合、以下のように代入文を使用します。
|biohazard| シンボルは、 医療廃棄物を収めたコンテナに 表示されるマークです。 .. |biohazard| image:: biohazard.png
ここでは、はてな記法のようにリンク作成支援のためのディレクティブを作成することにします。これから作成するディレクティブを使うと、以下のような記述が可能になります。
* google記法 : |google記法| * amazon記法 : |amazon記法| * wikipedia記法 : |wikipedia記法| * isbn記法 : |isbn記法| .. |google記法| google:: はてな .. |amazon記法| amazon:: はてな .. |wikipedia記法| wikipedia:: はてな_(企業) .. |isbn記法| isbn:: 4798110523 近藤(2006)
これをHTMLに変換すると以下のようになります。
<ul class="simple"> <li>google記法 : <a class="reference external" href="http://www.google.com/search?q=%E3%81%AF%E3%81%A6%E3%81%AA">google:はてな</a></li> <li>amazon記法 : <a class="reference external" href="http://www.amazon.co.jp/exec/obidos/external-search?keywords=%E3%81%AF%E3%81%A6%E3%81%AA&mode=blended">amazon:はてな</a></li> <li>wikipedia記法 : <a class="reference external" href="http://ja.wikipedia.org/wiki/%E3%81%AF%E3%81%A6%E3%81%AA_%28%E4%BC%81%E6%A5%AD%29">wikipedia:はてな_(企業)</a></li> <li>isbn記法 : <a class="reference external" href="http://www.amazon.co.jp/o/asin/4798110523/">近藤(2006)</a></li> </ul>
Docutilsのディレクティブを作成する方法はこちらのページで紹介されています。
基本的には、docutils.parsers.rst.Directiveを継承したクラスでrunメソッドを作りそこでdocutils.nodes.Nodeのサブクラスのインスタンスのリストを返すようにプログラムを組むようです。
HTMLのリンクに相当するノードはdocutils.nodes.referenceです。これは以下のプログラムで実際にreSTテキストからそれに対応するノードを調べることで確認することができます。
from docutils.core import publish_doctree doctree = publish_doctree("`Example <http://www.example.com>`_") print doctree
実行結果(見やすいように整形しています)
<document source="<string>"> <paragraph> <reference name="Example" refuri="http://www.example.com">Example</reference> <target ids="['example']" names="[u'example']" refuri="http://www.example.com"/> </paragraph> </document>
また、この実行結果からreferenceノードはrefuriという属性で参照先のURLを保持していることがわかります。
以上の情報とDocutilsのソースファイルを元に試行錯誤をした結果、以下のようなプログラムを作成しました。このプログラムを使用することで、上記の例のような出力を作成することができます。
import sys import urllib from docutils.core import publish_parts, publish_doctree from docutils import nodes from docutils.parsers.rst import directives from docutils.parsers.rst import Directive from docutils.parsers.rst import states class MyBaseDirective(Directive): def run(self): if not isinstance(self.state, states.SubstitutionDef): raise self.error( 'Invalid context: the "%s" directive can only be used within ' 'a substitution definition.' % self.name) title, refuri = self.run_impl() self.options['refuri'] = refuri self.options['name'] = title ref_node = nodes.reference(**self.options) ref_node += nodes.Text(title) return [ref_node] class Google(MyBaseDirective): required_arguments = 1 optional_arguments = 0 final_argument_whitespace = True has_content = False def run_impl(self): q = self.arguments[0] title = u"google:%s" % q refuri = "http://www.google.com/search?q=%s" % urllib.quote_plus(q.encode("utf-8")) return title, refuri class Amazon(MyBaseDirective): required_arguments = 1 optional_arguments = 0 final_argument_whitespace = True has_content = False def run_impl(self): keyword = self.arguments[0] query = {"mode" : "blended", "keywords" : keyword.encode("utf-8") } query_str = "&".join(("%s=%s" % (k, urllib.quote_plus(v)) for k, v in query.iteritems())) refuri = "http://www.amazon.co.jp/exec/obidos/external-search?%s" % query_str title = u"amazon:%s" % keyword return title, refuri class Wikipedia(MyBaseDirective): required_arguments = 1 optional_arguments = 0 final_argument_whitespace = True has_content = False def run_impl(self): wiki_title = self.arguments[0] title = u"wikipedia:%s" % wiki_title refuri = "http://ja.wikipedia.org/wiki/%s" % urllib.quote_plus(wiki_title.encode("utf-8")) return title, refuri class Isbn(MyBaseDirective): required_arguments = 2 optional_arguments = 0 final_argument_whitespace = True has_content = False def run_impl(self): asin = self.arguments[0] title = self.arguments[1] refuri = u"http://www.amazon.co.jp/o/asin/%s/" % asin return title, refuri directives.register_directive("google", Google) directives.register_directive("amazon", Amazon) directives.register_directive("wikipedia", Wikipedia) directives.register_directive("isbn", Isbn) if __name__ == '__main__': with open(sys.argv[1]) as io: value = io.read() parts = publish_parts(source=value, writer_name="html4css1") # html = parts["whole"] html = parts["fragment"] print html.encode("utf-8")