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*"
  1. Translatorを作成する
  2. translator(visitor)が何か操作を行う。
  3. 出力結果は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の話がしたかった(こちらが本命)。