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