XD blog

blog page

directive


2015-06-07 Custom Directive on Sphinx

I recently discovered a nice way to integrate plots in sphinx documentation with the custom directive bokeh-plot. I thought it would be quite easy to create mine to add a simple blogging system. However the documentation is pretty rare on that topic. All my searches ended up at Tutorial: Writing a simple extension. So here are my finding about creating a custom directive BlogPostDirective to process something like:

.. blogpost::
    :title: Migration to IPython 3.1
    :keywords: ipython, migration, jupyter, jenkins, pandoc
    :date: 2015-04-16
    :categories: ipython, documentation
    
    Any text this blog could contains and any RST tag::
    
        ...

Step 1: create a custom node

Sphinx converts a RST files into a tree, each nodes contains some text and some information on how to process it. It also contains children. After being converted into HTML, the same structure appears. All nodes comes from docutils.nodes.

from docutils import nodes

class blogpost_node(nodes.Structural, nodes.Element):
    pass

Step 2: create a custom directive

We define here the class which convert RST into a set of nodes. It contains static variables and overrides method run which produces a list of nodes such as blogpost_node.

from docutils.parsers.rst import Directive

class BlogPostDirective(Directive):

    # defines the parameter the directive expects
    # directives.unchanged means you get the raw value from RST
    required_arguments = 0
    optional_arguments = 0
    final_argument_whitespace = True
    option_spec = {'date': directives.unchanged,
                   'title': directives.unchanged,
                   'keywords': directives.unchanged,
                   'categories': directives.unchanged, }
    has_content = True
    add_index = True

    def run(self):
        sett = self.state.document.settings
        language_code = sett.language_code
        env = self.state.document.settings.env
        
        # gives you access to the parameter stored
        # in the main configuration file (conf.py)
        config = env.config
        
        # gives you access to the options of the directive
        options = self.options
        
        # we create a section
        idb = nodes.make_id("blog-" + options["date"] + "-" + options["title"])
        section = nodes.section(ids=[idb])
        
        # we create a title and we add it to section
        section += nodes.title(options["title"])
        
        # we create the content of the blog post
        # because it contains any kind of RST
        # we parse parse it with function nested_parse
        par = nodes.paragraph()
        self.state.nested_parse(content, self.content_offset, par)
        
        # we create a blogpost and we add the section
        node = blogpost_node()
        node += section
        node += par
        
        # we return the result
        return [ node ]

The important function is the method nested_parse which converts the raw RST into nodes for the documentation. Our method run just add a title before this conversion happens. You will discover others tricks in file sphinx_blog_extension.py.

Step 3: register the class and the nodes

This takes places in file conf.py.

from somewhere import BlogPostDirective
def setup(app):
    app.add_node(blogpost_node)
    app.add_directive('blogpost', BlogPostDirective)

It is all done but there are some others tricks you can use.

Step 4: register a new variable in the documentation

Still in the file conf.py (everything can be placed there). The value of the new variable can be retrieved from method run in the new directive.

def setup(app):
    app.add_config_value('my_new_variable', 'default_value', 'env')
    
my_new_variable = "another value"

Step 5: post process HTML

Imagine we now want to add HTML content before and after the blog was processed. We registered two functions Sphinx will call later during the process. For example, we add a link to something after the blog post content.

def visit_blogpost_node(self, node):
    pass

def depart_blogpost_node(self, node):
    link = """<p><a class="reference internal" href="something.html" title="a title">a title</a></p>"""
    self.body.append(link)

def setup(app):
    app.add_node(blogpost_node, html=(visit_blogpost_node, depart_blogpost_node))

Step 6: encapsulate a node into a div

Sphinx offers simple commands to insert the blog into a div (HTML).

def visit_blogpost_node(self, node):
    # this function adds "admonition" to the class name of tag div
    # it will look like a warning or a note
    self.visit_admonition(node)

def depart_blogpost_node(self, node):
    self.depart_admonition(node)

Good luck if you start your own directive, you will probably need a couple of tries before getting it right. You can also try running your class using function publish_programmatically. That's what I did in the constructor of class BlogPost.


Xavier Dupré