Extension Authors ----------------- In this section I will take you through an example of writing a new directive for the runestone interactive system. In this case we will look at adding a ``datafile`` directive. The ``datafile`` directive is going to be used in conjunction with the skulpt file operations. For skulpt to read a file we must provide some data that data can either be data that is part of a ``
`` element or if we want the data to be editable we can embed the data in a ``

We will also make this extension work if the user wants to generate a non-interactive paper or pdf version of some text using LaTeX.  In this case the output of our directive should simply be:

::

    \begin{verbatim}
    line 1  some data
    line 2  some more data
    line 3  this is the end
    \end{verbatim}
    
Unless the ``:hide:`` flag is present in which case we won't generate anything.


Components of a directive
~~~~~~~~~~~~~~~~~~~~~~~~~

There are several parts to writing an extension.

1.  Initialization:  In this part you need to let sphinx know that there there is a new directive.  The name of the directive is associated with a Python class that derives from the restructuredText ``Directive`` class.

2.  Parse Time:  In this stage of processing sphinx is building the document object model.  A tree of nodes.  At this stage we process all of the arguments and gather the main content into a structure that can be used later when rendering a document in whatever format:  html, latex, epub, etc.

3.  Output:  At this stage we take the data that we have and format it for output by whatever *writer* is being used, again html, latex, etc.


All of the components of a directive can go in a single Python file.  Sphinx sees these extensions as simple python packages, so you can create a folder with an ``__init.py`` file or you can create an ``__init__.py`` file that imports one or more of your own files.

The ``__init__.py`` file should go in the modules folder following the folder heirarchy that is in place.  For example  ``luther/sphinx/datafile/``.


Initialization
~~~~~~~~~~~~~~

For the initialization phase we need to provide a setup function.  The setup function connects the directive name with a class, and tells skulpt about any css or javscript files that will be needed to support any html output.

The initialization code for the datafile directive looks like this:

::

    from docutils import nodes
    from docutils.parsers.rst import directives
    from docutils.parsers.rst import Directive


    def setup(app):
        app.add_directive('datafile',DataFile)
        app.add_javascript('bookfuncs.js')
        app.add_javascript('skulpt/dist/skulpt.js')
        app.add_javascript('skulpt/dist/builtin.js')

        app.add_node(DataFileNode, html=(visit_df_node, depart_df_node))

        app.connect('doctree-resolved',process_datafile_nodes)
        app.connect('env-purge-doc', purge_datafiles)

The key function here is the setup function.   The ``add_directive`` method maps the ``datafile`` directive to the ``DataFile`` class.  Then we add three javascript files we want to include along with a new node type called ``DataFileNode``  This is the node that will get added to the document object model.

The next line connects the ``'doctree-resolved'`` event with a function called ``process_datafile_nodes``.  This function could be used to create an index of our datafiles, or do nothing.  the ``env-purge-doc`` event is simliar but is called after all datafile nodes have been processed.

Parsing
~~~~~~~

During the parse phase there is one class for every directive.  This class must supply a run method that returns a list of nodes.  If you know that your directive is only ever going to create output for a particular rendering, e.g. html you can finish your run method by returning a ``raw`` node that is simply a string.

::

    return [nodes.raw('', mystring ,format='html')]
    
In this case mystring could be as long and complicated an html string as you might like to make.  A minimal hello world type example might be:

::
  
    return [nodes.raw('', '

Hello World

', format='html')] In other cases you will defer the rendering of the node until later, having built a structure that can support multiple output styles. In our example you will see that we return: :: return [DataFileNode(self.options)] Where the DataFileNode just maintains a dictionary of data items we will use when we want to render any of the possible output formats. If you return a ``raw`` node then you can ignore the ``app.add_node(DataFileNode, html=(visit_df_node, depart_df_node))`` line in the ``setup`` method. Here is the DataFile class along with its run method. .. sourcecode:: python class DataFile(Directive): required_arguments = 1 optional_arguments = 2 has_content = True option_spec = { 'hide':directives.flag, 'edit':directives.flag, 'rows':directives.positive_int, 'cols':directives.positive_int } def run(self): env = self.state.document.settings.env if not hasattr(env,'datafilecounter'): env.datafilecounter = 0 env.datafilecounter += 1 if 'cols' not in self.options: self.options['cols'] = min(65,max([len(x) for x in self.content])) if 'rows'not in self.options: self.options['rows'] = 20 self.options['divid'] = self.arguments[0] if self.content: source = "\n".join(self.content) else: source = '\n' self.options['filecontent'] = source if 'hide' not in self.options: self.options['hide'] = 'block' else: self.options['hide'] = 'none' if 'edit' not in self.options: self.options['edit'] = False return [DataFileNode(self.options)] As you can see our class has some shared/static/class level variables that specify whether the directive has content, how many arguments, and what the names and types of the arguments are. Here are the possible types for the options: * flag: For options with no option arguments. Checks for an argument (raises ValueError if found), returns None for valid flag options. * unchanged_required: Returns the text argument, unchanged. Raises ValueError if no argument is found. * unchanged: Returns the text argument, unchanged. Returns an empty string ("") if no argument is found. * path: Returns the path argument unwrapped (with newlines removed). Raises ValueError if no argument is found. * uri: Returns the URI argument with whitespace removed. Raises ValueError if no argument is found. * nonnegative_int: Checks for a nonnegative integer argument, and raises ValueError if not. * class_option: Converts the argument into an ID-compatible string and returns it. Raises ValueError if no argument is found. * unicode_code: Convert a Unicode character code to a Unicode character. * single_char_or_unicode: A single character is returned as-is. Unicode characters codes are converted as in unicode_code. * single_char_or_whitespace_or_unicode: As with single_char_or_unicode, but "tab" and "space" are also supported. * positive_int: Converts the argument into an integer. Raises ValueError for negative, zero, or non-integer values. * positive_int_list: Converts a space- or comma-separated list of integers into a Python list of integers. Raises ValueError for non-positive-integer values. * encoding: Verfies the encoding argument by lookup. Raises ValueError for unknown encodings. There are two important instance variables to think about. * self.content Refers to the body content of the directive. That is the indented text that comes after the options. This can be anything, and you can make up your own conventions for how to interpret the body of a directive. * self.options Is a dictionary that contins key value pairs for all of the optional parameters. Of course you are free to modify this dictionary as much as you wish. The ``self.options`` instance variable is a dictionary that will be used later to fill in a template for our output. So, most of the processing in the ``run`` method is checking to see if the author provided values for these options and providing some default values for them if they have not. The last line of ``run`` creates an instance of our DataFileNode. This object doesn't do much other than act as a container to hold our data until it is time to render the output. Nevertheless here is the code: :: class DataFileNode(nodes.General, nodes.Element): def __init__(self,content): """ Arguments: - `self`: - `content`: """ super(DataFileNode,self).__init__() self.df_content = content Output ~~~~~~ In the final stage of processing we render the document in whatever format the author is requesting. In this stage we take the data stored away in our ``DataFileNode`` and turn it into html or latex or whatever. Each output format is handled by their own function that is registered back in the setup phase. recall that we used the following line to connect our html output with a couple of functions: ``app.add_node(DataFileNode, html=(visit_df_node, depart_df_node))``. We will use Python triple quoted strings to create a couple of templates to fill in. In the code below these templates are ``PRE`` and ``TEXTA`` :: PRE = '''
    %(filecontent)s
    
''' TEXTA = ''' ''' # self for these functions is an instance of the writer class. For example # in html, self is sphinx.writers.html.SmartyPantsHTMLTranslator # The node that is passed as a parameter is an instance of our node class. def visit_df_node(self,node): if node.df_content['edit'] == True: res = TEXTA else: res = PRE res = res % node.df_content res = res.replace("u'","'") # hack: there must be a better way to include the list and avoid unicode strings self.body.append(res) def depart_ac_node(self,node): ''' This is called at the start of processing an activecode node. If activecode had recursive nodes etc and did not want to do all of the processing in visit_ac_node any finishing touches could be added here. ''' pass The ``visit_df_node`` function is called when a DataFileNode is found in the document tree. The visit function is called when the node is first encountered. The ``depart_df_node`` function is called after all the children have been visited. Notice that ``visit_df_node`` mainly just looks at its options to see if we need to create a textarea or a pre element and then lets Python's formatted strings take care of the rest.