docutilsを直接利用してRestからmarkdown(のsubset)を生成するツールを作ってみる
docutils
ReSTのためのライブラリ
ReST?
markdownのようなもの
first step
docutilsではpublisherと呼ばれるものがもっともトップレベルのapplication。 内部でドキュメントをparseしてnode treeを作成した後に各writerのtranslateで出力される。
reader+parser -> node tree -> {Writer0,Writer1,Writer2}
本当はもう少し細かく内部で色々分かれているのだけれど。とりあえず以下が分かれば良い。
- parserがparseしたらnode treeが作られる
- writerはnode treeを走査してoutputを作る
null writer
最もシンプルな入力と処理は以下の様なもの
00app.py
from docutils.writers.null import Writer from docutils.core import publish_cmdline publish_cmdline(writer=Writer())
例えば以下のような入力を渡すとする。
00hello.rst
hello title ======================================== hello section ---------------------------------------- hello text
null.Writerは何もしない。
$ python 00app.py --traceback src/00hello.rst
とは言え内部で何が行われているかわからないとどうしようもない。--debug
を付けてみる
$ python 00app.py --debug --traceback src/00hello.rst StateMachine.run: input_lines (line_offset=-1): | hello title | ======================================== | | hello section | ---------------------------------------- | | hello text StateMachine.run: bof transition ... snip src/00hello.rst:: (DEBUG/0) docutils.nodes.Node.walk calling dispatch_visit for document src/00hello.rst:: (DEBUG/0) docutils.nodes.NodeVisitor.dispatch_visit calling _nop for document src/00hello.rst:: (DEBUG/0) docutils.nodes.Node.walk calling dispatch_visit for title src/00hello.rst:: (DEBUG/0) docutils.nodes.NodeVisitor.dispatch_visit calling _nop for title src/00hello.rst:: (DEBUG/0) docutils.nodes.Node.walk calling dispatch_visit for Text src/00hello.rst:: (DEBUG/0) docutils.nodes.NodeVisitor.dispatch_visit calling _nop for Text src/00hello.rst:: (DEBUG/0) docutils.nodes.Node.walk calling dispatch_visit for subtitle src/00hello.rst:: (DEBUG/0) docutils.nodes.NodeVisitor.dispatch_visit calling _nop for subtitle src/00hello.rst:: (DEBUG/0) docutils.nodes.Node.walk calling dispatch_visit for Text src/00hello.rst:: (DEBUG/0) docutils.nodes.NodeVisitor.dispatch_visit calling _nop for Text src/00hello.rst:: (DEBUG/0) docutils.nodes.Node.walk calling dispatch_visit for paragraph src/00hello.rst:: (DEBUG/0) docutils.nodes.NodeVisitor.dispatch_visit calling _nop for paragraph src/00hello.rst:: (DEBUG/0) docutils.nodes.Node.walk calling dispatch_visit for Text src/00hello.rst:: (DEBUG/0) docutils.nodes.NodeVisitor.dispatch_visit calling _nop for Text
何か生成されたnodeを走査してそうなことが分かる。
writerを自作してみる
writerを自作してみる。通常writerはおおよそ以下の様な構造になっている。
from docutils.writers import Writer class Writer(Writer): def __init__(self): super().__init__() self.translator_class = Translator def translate(self): self.visitor = visitor = self.translator_class(self.document) self.document.walkabout(visitor) self.output = "*hmm*"
- Translatorを作成する
- translator(visitor)が何か操作を行う。
- 出力結果はself.outputに含まれる
例えば以下の様なTranslatorを用意してみる。
from docutils import nodes class Translator(nodes.NodeVisitor): def __init__(self, document): super().__init__(document) self._depth = 0 def dispatch_visit(self, node): self._depth += 1 return super().dispatch_visit(node) def dispatch_departure(self, node): r = super().dispatch_departure(node) self._depth -= 1 return r def unknown_visit(self, node): i = self._depth nodename = node.__class__.__name__ logger.debug("%svisit %s[%d] :%s", " " * i, nodename, i, node) def unknown_departure(self, node): i = self._depth nodename = node.__class__.__name__ logger.debug("%sdeparture %s[%d] :%s", " " * i, nodename, i, node)
unknown_visit()
と unknown_departure()
Visitorが渡されたNodeに対応するメソッドを持っていない時に呼ばれるメソッド。ここでログ出力を行うことにしたのでvisitorが走査される様がどのようなものか確認できる。
以下の様な感じ。
DEBUG:__main__: visit document[1] :<document ids="hello-title" names="hello\ title" source="src/00hello.rst" title="hello title"><title>hello title</title><subtitle ids="hello-section" names="hello\ section">hello section</subtitle><paragraph>hello text</paragraph></document> DEBUG:__main__: visit title[2] :<title>hello title</title> DEBUG:__main__: visit Text[3] :hello title DEBUG:__main__: departure Text[3] :hello title DEBUG:__main__: departure title[2] :<title>hello title</title> DEBUG:__main__: visit subtitle[2] :<subtitle ids="hello-section" names="hello\ section">hello section</subtitle> DEBUG:__main__: visit Text[3] :hello section DEBUG:__main__: departure Text[3] :hello section DEBUG:__main__: departure subtitle[2] :<subtitle ids="hello-section" names="hello\ section">hello section</subtitle> DEBUG:__main__: visit paragraph[2] :<paragraph>hello text</paragraph> DEBUG:__main__: visit Text[3] :hello text DEBUG:__main__: departure Text[3] :hello text DEBUG:__main__: departure paragraph[2] :<paragraph>hello text</paragraph> DEBUG:__main__: departure document[1] :<document ids="hello-title" names="hello\ title" source="src/00hello.rst" title="hello title"><title>hello title</title><subtitle ids="hello-section" names="hello\ section">hello section</subtitle><paragraph>hello text</paragraph></document> *hmm*
visit_title()
, visit_subtitle()
などが呼ばれ後にvisitTextが呼ばれるみたいな構造になっていることが分かる。
そのあと writer.output
に代入された "*hmm*"
の値が表示されている(こちらは標準出力)
そんなわけで後はvisitorをどのように定義すれば良いかということだけがわかればmarkdownへの変更は難しく無さそう。
自作のmarkdown writer
とりあえず以下の文章を変換できるようにしてみる
src/02sample.rst
タイトル ======================================== 何か文章 サブタイトル ---------------------------------------- 本文本文本文本文本文本文本文本文本文本文本文本文本文本文(ここでお経を唱えると功徳が積める) サブタイトル2 ---------------------------------------- 箇条書き - item0 - item1 - item2
こういう感じに
$ python 02app.py --traceback src/02sample.rst
変換後は以下のようなmarkdownに
# タイトル 何か文章 ## サブタイトル 本文本文本文本文本文本文本文本文本文本文本文本文本文本文(ここでお経を唱えると功徳が積める) ## サブタイトル2 箇条書き - item0 - item1 - item2
code
02app.py
from io import StringIO from docutils.writers import Writer from docutils import nodes class Writer(Writer): def __init__(self): super().__init__() self.translator_class = MarkdownTranslator def translate(self): self.visitor = visitor = self.translator_class(self.document) self.document.walkabout(visitor) self.output = visitor.io.getvalue().rstrip("\n") class MarkdownTranslator(nodes.NodeVisitor): def __init__(self, document): super().__init__(document) self.io = StringIO() self.section_level = 1 def visit_section(self, node): self.section_level += 1 def depart_section(self, node): self.section_level -= 1 def visit_title(self, node): if self.section_level > 1: self.io.write("\n") self.io.write("#" * self.section_level) self.io.write(" ") self.io.write(node.astext()) self.io.write("\n") raise nodes.SkipNode def visit_paragraph(self, node): self.io.write('\n') def depart_paragraph(self, node): self.io.write('\n') def visit_Text(self, node): self.io.write(node.astext()) def visit_list_item(self, node): self.io.write("- ") for c in node.children: self.io.write(c.astext()) self.io.write('\n') raise nodes.SkipNode def unknown_visit(self, node): pass def unknown_departure(self, node): pass if __name__ == "__main__": from docutils.core import publish_cmdline publish_cmdline(writer=Writer())
本当は
本当はdirectivesとかroleの話がしたかった(こちらが本命)。