source: specdomain/src/specdomain/sphinxcontrib/specdomain.py @ 942

Last change on this file since 942 was 942, checked in by jemian, 10 years ago

refs #8, apply ANL's short-form license, starting to understand how to get content added, try additional test case SPEC macro file

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
File size: 13.0 KB
Line 
1# -*- coding: utf-8 -*-
2"""
3    sphinxcontrib.specdomain
4    ~~~~~~~~~~~~~~~~~~~~~~~~~~
5
6    :synopsis: SPEC domain for Sphinx
7   
8    Automatically insert ReST-formatted extended comments
9    from SPEC files for macro definitions and variable declarations into
10    the Sphinx doctree, thus avoiding duplication between docstrings and documentation
11    for those who like elaborate docstrings.
12
13    :copyright: Copyright 2012 by BCDA, Advanced Photon Source, Argonne National Laboratory
14    :license: ANL Open Source License, see LICENSE for details.
15"""
16
17# http://sphinx.pocoo.org/ext/appapi.html
18
19import os
20import re
21import string                                           #@UnusedImport
22
23from docutils import nodes                              #@UnusedImport
24from docutils.parsers.rst import directives             #@UnusedImport
25
26from sphinx import addnodes
27from sphinx.roles import XRefRole
28from sphinx.locale import l_, _                         #@UnusedImport
29from sphinx.directives import ObjectDescription
30from sphinx.domains import Domain, ObjType, Index       #@UnusedImport
31from sphinx.util.compat import Directive                #@UnusedImport
32from sphinx.util.nodes import make_refnode, nested_parse_with_titles
33from sphinx.util.docfields import Field, TypedField
34from sphinx.util.docstrings import prepare_docstring    #@UnusedImport
35
36from docutils.statemachine import ViewList, string2lines
37import sphinx.util.nodes
38
39
40match_all                   = r'.*'
41non_greedy_filler           = match_all + r'?'
42double_quote_string_match   = r'("' + non_greedy_filler + r'")'
43word_match                  = r'((?:[a-z_]\w*))'
44cdef_match                  = r'(cdef)'
45extended_comment_flag       = r'\"\"\"'
46
47
48spec_macro_sig_re = re.compile(
49                               r'''^ ([a-zA-Z_]\w*)         # macro name
50                               ''', re.VERBOSE)
51
52spec_func_sig_re = re.compile(word_match + r'\('
53                      + r'(' + match_all + r')' 
54                      + r'\)', 
55                      re.IGNORECASE|re.DOTALL)
56
57spec_cdef_name_sig_re = re.compile(double_quote_string_match, 
58                                   re.IGNORECASE|re.DOTALL)
59
60
61spec_extended_comment_flag_sig_re = re.compile(extended_comment_flag, 
62                                               re.IGNORECASE|re.DOTALL)
63spec_extended_comment_start_sig_re = re.compile(r'^'
64                                                + non_greedy_filler
65                                                + extended_comment_flag, 
66                                                re.IGNORECASE|re.DOTALL)
67spec_extended_comment_block_sig_re = re.compile(r'^'
68                                                + non_greedy_filler
69                                                + extended_comment_flag
70                                                + r'(' + non_greedy_filler + r')'
71                                                + extended_comment_flag
72                                                + non_greedy_filler
73                                                + r'$', 
74                                                re.IGNORECASE|re.DOTALL|re.MULTILINE)
75
76
77class SpecMacroObject(ObjectDescription):
78    """
79    Description of a SPEC macro definition
80    """
81
82    doc_field_types = [
83        TypedField('parameter', label=l_('Parameters'),
84                   names=('param', 'parameter', 'arg', 'argument',
85                          'keyword', 'kwarg', 'kwparam'),
86                   typerolename='def', typenames=('paramtype', 'type'),
87                   can_collapse=True),
88        Field('returnvalue', label=l_('Returns'), has_arg=False,
89              names=('returns', 'return')),
90        Field('returntype', label=l_('Return type'), has_arg=False,
91              names=('rtype',)),
92    ]
93
94    def add_target_and_index(self, name, sig, signode):
95        targetname = '%s-%s' % (self.objtype, name)
96        signode['ids'].append(targetname)
97        self.state.document.note_explicit_target(signode)
98        indextext = self._get_index_text(name)
99        if indextext:
100            self.indexnode['entries'].append(('single', indextext,
101                                              targetname, ''))
102
103    def _get_index_text(self, name):
104        macro_types = {
105            'def':  'SPEC macro definition; %s',
106            'rdef': 'SPEC run-time macro definition; %s',
107            'cdef': 'SPEC chained macro definition; %s',
108        }
109        if self.objtype in macro_types:
110            return _(macro_types[self.objtype]) % name
111        else:
112            return ''
113
114    def handle_signature(self, sig, signode):
115        # Must be able to match these (without preceding def or rdef)
116        #     def macro_name
117        #     def macro_name()
118        #     def macro_name(arg1, arg2)
119        #     rdef macro_name
120        #     cdef("macro_name", "content", "groupname", flags)
121        m = spec_func_sig_re.match(sig) or spec_macro_sig_re.match(sig)
122        if m is None:
123            raise ValueError
124        arglist = m.groups()
125        name = arglist[0]
126        args = []
127        if len(arglist) > 1:
128            args = arglist[1:]
129            if name == 'cdef':
130                # TODO: need to match complete arg list
131                # several different signatures are possible (see cdef-examples.mac)
132                # for now, just get the macro name and ignore the arg list
133                m = spec_cdef_name_sig_re.match(args[0])
134                arglist = m.groups()
135                name = arglist[0].strip('"')
136                args = ['<<< cdef argument list not handled yet >>>']       # FIXME:
137        signode += addnodes.desc_name(name, name)
138        if len(args) > 0:
139            signode += addnodes.desc_addname(args, args)
140        return name
141
142
143class SpecVariableObject(ObjectDescription):
144    """
145    Description of a SPEC variable
146    """
147
148
149class SpecMacroSourceObject(ObjectDescription):
150    """
151    Document a SPEC macro source code file
152   
153    This code responds to the ReST file directive::
154   
155        .. spec:macrofile:: partial/path/name/somefile.mac
156            :displayorder: fileorder
157   
158    The ``:displayorder`` parameter indicates how the
159    contents will be sorted for appearance in the ReST document.
160   
161        **fileorder**, **file**
162            Items will be documented in the order in
163            which they appear in the ``.mac`` file.
164       
165        **alphabetical**, **alpha**
166            Items will be documented in alphabetical order.
167   
168    A (near) future enhancement would be to provide for
169    documenting all macro files in a directory, with optional
170    recursion into subdirectories.  By default, the code would
171    only document files that match the glob pattern ``*.mac``.
172    Such as::
173   
174       .. spec:directory:: partial/path/name
175          :recursion:
176          :displayorder: alphabetical
177    """
178   
179    # TODO: work-in-progress
180   
181    doc_field_types = [
182        Field('displayorder', label=l_('Display order'), has_arg=False,
183              names=('displayorder', 'synonym')),
184    ]
185
186    def add_target_and_index(self, name, sig, signode):
187        targetname = '%s-%s' % (self.objtype, name)
188        signode['ids'].append(targetname)
189        self.state.document.note_explicit_target(signode)
190        indextext = sig
191        if indextext:
192            self.indexnode['entries'].append(('single', indextext,
193                                              targetname, ''))
194
195    def handle_signature(self, sig, signode):
196        signode += addnodes.desc_name(sig, sig)
197        # TODO: this is the place to parse the SPEC macro source code file named in "sig"
198        '''
199        Since 2002, SPEC has allowed for triple-quoted strings as extended comments.
200        Few, if any, have used them.
201        Assume that they will contain ReST formatted comments.
202        The first, simplest thing to do is to read the .mac file and only extract
203        all the extended comments and add them as nodes to the current document.
204       
205        An additional step would be to parse for def, cdef, rdef, global, local, const, ...
206        Another step would be to attach source code and provide links from each to
207        highlighted source code blocks.
208        '''
209        extended_comments_list = self.parse_macro_file(sig)
210        view = ViewList([u'TODO: recognize the ReST formatting in the following extended comment and it needs to be cleaned up'])
211        node = nodes.paragraph()
212        node.document = self.state.document
213        self.state.nested_parse(view, 0, signode)
214        # TODO: recognize the ReST formatting in the following extended comment and it needs to be cleaned up
215        for extended_comment in extended_comments_list:
216            for line in string2lines(extended_comment):
217                view = ViewList([line])
218                nested_parse_with_titles(self.state, view, signode)
219        return sig
220   
221    def parse_macro_file(self, filename):
222        """
223        parse the SPEC macro file and return the ReST blocks
224       
225        :param str filename: name (with optional path) of SPEC macro file
226            (The path is relative to the ``.rst`` document.)
227        :returns [str]: list of ReST-formatted extended comment blocks (docstrings) from SPEC macro file.
228       
229        [future] parse more stuff as planned, this is very simplistic for now
230        """
231        results = []
232        if not os.path.exists(filename):
233            raise RuntimeError, "could not find: " + filename
234       
235        buf = open(filename, 'r').read()
236        #n = len(buf)
237        for node in spec_extended_comment_block_sig_re.finditer(buf):
238            #g = node.group()
239            #gs = node.groups()
240            #s = node.start()
241            #e = node.end()
242            #t = buf[s:e]
243            results.append(node.groups()[0])            # TODO: can we get line number also?
244        return results
245
246
247class SpecXRefRole(XRefRole):
248    """ """
249   
250    def process_link(self, env, refnode, has_explicit_title, title, target):
251        key = ":".join((refnode['refdomain'], refnode['reftype']))
252        refnode[key] = env.temp_data.get(key)        # key was 'spec:def'
253        if not has_explicit_title:
254            title = title.lstrip(':')   # only has a meaning for the target
255            target = target.lstrip('~') # only has a meaning for the title
256            # if the first character is a tilde, don't display the module/class
257            # parts of the contents
258            if title[0:1] == '~':
259                title = title[1:]
260                colon = title.rfind(':')
261                if colon != -1:
262                    title = title[colon+1:]
263        return title, target
264
265    def result_nodes(self, document, env, node, is_ref):
266        # this code adds index entries for each role instance
267        if not is_ref:
268            return [node], []
269        varname = node['reftarget']
270        tgtid = 'index-%s' % env.new_serialno('index')
271        indexnode = addnodes.index()
272        indexnode['entries'] = [
273            ('single', varname, tgtid, ''),
274            #('single', _('environment variable; %s') % varname, tgtid, ''),
275        ]
276        targetnode = nodes.target('', '', ids=[tgtid])
277        document.note_explicit_target(targetnode)
278        return [indexnode, targetnode, node], []
279
280
281class SpecDomain(Domain):
282    """SPEC language domain."""
283   
284    name = 'spec'
285    label = 'SPEC, http://www.certif.com'
286    object_types = {    # type of object that a domain can document
287        'def':    ObjType(l_('def'),    'def'),
288        'rdef':   ObjType(l_('rdef'),   'rdef'),
289        'cdef':   ObjType(l_('cdef'),   'cdef'),
290        'global': ObjType(l_('global'), 'global'),
291        'local':  ObjType(l_('local'),  'local'),
292    }
293    directives = {
294        'def':          SpecMacroObject,
295        'rdef':         SpecMacroObject,
296        'cdef':         SpecMacroObject,
297        'global':       SpecVariableObject,
298        'local':        SpecVariableObject,
299        'macrofile':    SpecMacroSourceObject,
300    }
301    roles = {
302        'def' :     SpecXRefRole(),
303        'rdef':     SpecXRefRole(),
304        'cdef':     SpecXRefRole(),
305        'global':   SpecXRefRole(),
306        'local':    SpecXRefRole(),
307    }
308    initial_data = {
309        'objects': {}, # fullname -> docname, objtype
310    }
311
312    def clear_doc(self, docname):
313        for (typ, name), doc in self.data['objects'].items():
314            if doc == docname:
315                del self.data['objects'][typ, name]
316
317    def resolve_xref(self, env, fromdocname, builder, typ, target, node,
318                     contnode):
319        objects = self.data['objects']
320        objtypes = self.objtypes_for_role(typ)
321        for objtype in objtypes:
322            if (objtype, target) in objects:
323                return make_refnode(builder, fromdocname,
324                                    objects[objtype, target],
325                                    objtype + '-' + target,
326                                    contnode, target + ' ' + objtype)
327
328    def get_objects(self):
329        for (typ, name), docname in self.data['objects'].iteritems():
330            yield name, name, typ, docname, typ + '-' + name, 1
331
332
333# http://sphinx.pocoo.org/ext/tutorial.html#the-setup-function
334
335def setup(app):
336    app.add_domain(SpecDomain)
337    # http://sphinx.pocoo.org/ext/appapi.html#sphinx.domains.Domain
Note: See TracBrowser for help on using the repository browser.