2 # Copyright (C) 2006-2022 CEA/DEN, EDF R&D
4 # This library is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU Lesser General Public
6 # License as published by the Free Software Foundation; either
7 # version 2.1 of the License, or (at your option) any later version.
9 # This library is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # Lesser General Public License for more details.
14 # You should have received a copy of the GNU Lesser General Public
15 # License along with this library; if not, write to the Free Software
16 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 # See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
21 """Doxygen XML to SWIG docstring converter.
23 Converts Doxygen generated XML files into a file containing docstrings
24 that can be used by SWIG-1.3.x. Note that you need to get SWIG
25 version > 1.3.23 or use Robin Dunn's docstring patch to be able to use
28 input.xml is your doxygen generated XML file and output.i is where the
29 output will be written (the file will be clobbered).
33 __usage__ = "doxy2swig.py [options] input.xml output.i"
34 ######################################################################
36 # This code is implemented using Mark Pilgrim's code as a guideline:
37 # http://www.faqs.org/docs/diveintopython/kgp_divein.html
39 # Author: Prabhu Ramachandran
43 # Johan Hake: the include_function_definition feature
44 # Bill Spotz: bug reports and testing.
46 ######################################################################
48 from xml.dom import minidom
57 def my_open_read(source):
58 if hasattr(source, "read"):
61 return open(source, encoding='utf8')
63 def my_open_write(dest):
64 if hasattr(dest, "write"):
67 return open(dest, 'w', encoding='utf8')
71 """Converts Doxygen generated XML files into a file containing
72 docstrings that can be used by SWIG-1.3.x that have support for
73 feature("docstring"). Once the data is parsed it is stored in
78 def __init__(self, src, include_function_definition=True, quiet=False):
79 """Initialize the instance given a source object. `src` can
80 be a file or filename. If you do not want to include function
81 definitions from doxygen then set
82 `include_function_definition` to `False`. This is handy since
83 this allows you to use the swig generated function definition
84 using %feature("autodoc", [0,1]).
88 self.my_dir = os.path.dirname(f.name)
89 self.xmldoc = minidom.parse(f).documentElement
93 self.pieces.append('\n// File: %s\n'%\
94 os.path.basename(f.name))
96 self.space_re = re.compile(r'\s+')
97 self.lead_spc = re.compile(r'^(%feature\S+\s+\S+\s*?)"\s+(\S)')
99 self.ignores = ['inheritancegraph', 'param', 'listofallmembers',
100 'innerclass', 'name', 'declname', 'incdepgraph',
101 'invincdepgraph', 'programlisting', 'type',
102 'references', 'referencedby', 'location',
103 'collaborationgraph', 'reimplements',
104 'reimplementedby', 'derivedcompoundref',
107 self.include_function_definition = include_function_definition
108 if not include_function_definition:
109 self.ignores.append('argsstring')
115 """Parses the file set in the initialization. The resulting
116 data is stored in `self.pieces`.
119 self.parse(self.xmldoc)
121 def parse(self, node):
122 """Parse a given node. This function in turn calls the
123 `parse_<nodeType>` functions which handle the respective
127 pm = getattr(self, "parse_%s"%node.__class__.__name__)
130 def parse_Document(self, node):
131 self.parse(node.documentElement)
133 def parse_Text(self, node):
135 txt = txt.replace('\\', r'\\\\')
136 txt = txt.replace('"', r'\"')
137 # ignore pure whitespace
138 m = self.space_re.match(txt)
139 if m and len(m.group()) == len(txt):
142 self.add_text(textwrap.fill(txt, break_long_words=False))
144 def parse_Element(self, node):
145 """Parse an `ELEMENT_NODE`. This calls specific
146 `do_<tagName>` handers for different elements. If no handler
147 is available the `generic_parse` method is called. All
148 tagNames specified in `self.ignores` are simply ignored.
152 ignores = self.ignores
155 attr = "do_%s" % name
156 if hasattr(self, attr):
157 handlerMethod = getattr(self, attr)
160 self.generic_parse(node)
161 #if name not in self.generics: self.generics.append(name)
163 def parse_Comment(self, node):
164 """Parse a `COMMENT_NODE`. This does nothing for now."""
167 def add_text(self, value):
168 """Adds text corresponding to `value` into `self.pieces`."""
169 if type(value) in (list, tuple):
170 self.pieces.extend(value)
172 self.pieces.append(value)
174 def get_specific_nodes(self, node, names):
175 """Given a node and a sequence of strings in `names`, return a
176 dictionary containing the names as keys and child
177 `ELEMENT_NODEs`, that have a `tagName` equal to the name.
180 nodes = [(x.tagName, x) for x in node.childNodes \
181 if x.nodeType == x.ELEMENT_NODE and \
185 def generic_parse(self, node, pad=0):
186 """A Generic parser for arbitrary tags in a node.
190 - node: A node in the DOM.
191 - pad: `int` (default: 0)
193 If 0 the node data is not padded with newlines. If 1 it
194 appends a newline after parsing the childNodes. If 2 it
195 pads before and after the nodes are processed. Defaults to
201 npiece = len(self.pieces)
204 for n in node.childNodes:
207 if len(self.pieces) > npiece:
210 def space_parse(self, node):
212 self.generic_parse(node)
215 do_emphasis = space_parse
216 do_bold = space_parse
217 do_computeroutput = space_parse
218 do_formula = space_parse
220 def do_compoundname(self, node):
221 self.add_text('\n\n')
222 data = node.firstChild.data
223 self.add_text('%%feature("docstring") %s "\n'%data)
225 def do_compounddef(self, node):
226 kind = node.attributes['kind'].value
227 if kind in ('class', 'struct'):
228 prot = node.attributes['prot'].value
231 names = ('compoundname', 'briefdescription',
232 'detaileddescription', 'includes')
233 first = self.get_specific_nodes(node, names)
237 self.add_text(['";','\n'])
238 for n in node.childNodes:
239 if n not in list(first.values()):
241 elif kind in ('file', 'namespace'):
242 nodes = node.getElementsByTagName('sectiondef')
246 def do_includes(self, node):
247 self.add_text('C++ includes: ')
248 self.generic_parse(node, pad=1)
250 def do_parameterlist(self, node):
252 for key, val in list(node.attributes.items()):
254 if val == 'param': text = 'Parameters'
255 elif val == 'exception': text = 'Exceptions'
258 self.add_text(['\n', '\n', text, ':', '\n'])
259 self.generic_parse(node, pad=1)
261 def do_para(self, node):
263 self.generic_parse(node, pad=1)
265 def do_parametername(self, node):
268 data=node.firstChild.data
269 except AttributeError: # perhaps a <ref> tag in it
270 data=node.firstChild.firstChild.data
271 if data.find('Exception') != -1:
274 self.add_text("%s: "%data)
276 def do_parameterdefinition(self, node):
277 self.generic_parse(node, pad=1)
279 def do_detaileddescription(self, node):
280 self.generic_parse(node, pad=1)
282 def do_briefdescription(self, node):
283 self.generic_parse(node, pad=1)
285 def do_memberdef(self, node):
286 prot = node.attributes['prot'].value
287 id = node.attributes['id'].value
288 kind = node.attributes['kind'].value
289 tmp = node.parentNode.parentNode.parentNode
290 compdef = tmp.getElementsByTagName('compounddef')[0]
291 cdef_kind = compdef.attributes['kind'].value
294 first = self.get_specific_nodes(node, ('definition', 'name'))
295 name = first['name'].firstChild.data
296 if name[:8] == 'operator': # Don't handle operators yet.
299 if 'definition' not in first or \
300 kind in ['variable', 'typedef']:
303 if self.include_function_definition:
304 defn = first['definition'].firstChild.data
308 self.add_text('%feature("docstring") ')
310 anc = node.parentNode.parentNode
311 if cdef_kind in ('file', 'namespace'):
312 ns_node = anc.getElementsByTagName('innernamespace')
313 if not ns_node and cdef_kind == 'namespace':
314 ns_node = anc.getElementsByTagName('compoundname')
316 ns = ns_node[0].firstChild.data
317 self.add_text(' %s::%s "\n%s'%(ns, name, defn))
319 self.add_text(' %s "\n%s'%(name, defn))
320 elif cdef_kind in ('class', 'struct'):
321 # Get the full function name.
322 anc_node = anc.getElementsByTagName('compoundname')
323 cname = anc_node[0].firstChild.data
324 self.add_text(' %s::%s "\n%s'%(cname, name, defn))
326 for n in node.childNodes:
327 if n not in list(first.values()):
329 self.add_text(['";', '\n'])
331 def do_definition(self, node):
332 data = node.firstChild.data
333 self.add_text('%s "\n%s'%(data, data))
335 def do_sectiondef(self, node):
336 kind = node.attributes['kind'].value
337 if kind in ('public-func', 'func', 'user-defined', ''):
338 self.generic_parse(node)
340 def do_header(self, node):
341 """For a user defined section def a header field is present
342 which should not be printed as such, so we comment it in the
344 data = node.firstChild.data
345 self.add_text('\n/*\n %s \n*/\n'%data)
346 # If our immediate sibling is a 'description' node then we
347 # should comment that out also and remove it from the parent
349 parent = node.parentNode
350 idx = parent.childNodes.index(node)
351 if len(parent.childNodes) >= idx + 2:
352 nd = parent.childNodes[idx+2]
353 if nd.nodeName == 'description':
354 nd = parent.removeChild(nd)
355 self.add_text('\n/*')
356 self.generic_parse(nd)
357 self.add_text('\n*/\n')
359 def do_simplesect(self, node):
360 kind = node.attributes['kind'].value
361 if kind in ('date', 'rcs', 'version'):
363 elif kind == 'warning':
364 self.add_text(['\n', 'WARNING: '])
365 self.generic_parse(node)
368 self.add_text('See: ')
369 self.generic_parse(node)
371 self.generic_parse(node)
373 def do_argsstring(self, node):
374 self.generic_parse(node, pad=1)
376 def do_member(self, node):
377 kind = node.attributes['kind'].value
378 refid = node.attributes['refid'].value
379 if kind == 'function' and refid[:9] == 'namespace':
380 self.generic_parse(node)
382 def do_doxygenindex(self, node):
384 comps = node.getElementsByTagName('compound')
386 refid = c.attributes['refid'].value
387 fname = refid + '.xml'
388 if not os.path.exists(fname):
389 fname = os.path.join(self.my_dir, fname)
391 print("parsing file: %s"%fname)
392 p = Doxy2SWIG(fname, self.include_function_definition, self.quiet)
394 self.pieces.extend(self.clean_pieces(p.pieces))
396 def write(self, fname):
397 o = my_open_write(fname)
399 o.write("".join(self.pieces))
401 o.write("".join(self.clean_pieces(self.pieces)))
404 def clean_pieces(self, pieces):
405 """Cleans the list of strings given as `pieces`. It replaces
406 multiple newlines by a maximum of 2 and returns a new list.
407 It also wraps the paragraphs nicely.
422 ret.append('\n'*count)
428 for i in _data.split('\n\n'):
429 if i == 'Parameters:' or i == 'Exceptions:':
430 ret.extend([i, '\n-----------', '\n\n'])
431 elif i.find('// File:') > -1: # leave comments alone.
432 ret.extend([i, '\n'])
434 _tmp = textwrap.fill(i.strip(), break_long_words=False)
435 _tmp = self.lead_spc.sub(r'\1"\2', _tmp)
436 ret.extend([_tmp, '\n\n'])
440 def convert(input, output, include_function_definition=True, quiet=False):
441 p = Doxy2SWIG(input, include_function_definition, quiet)
446 parser = argparse.ArgumentParser(description=__doc__, usage = __usage__)
447 parser.add_argument("-n", '--no-function-definition',
451 help='do not include doxygen function definitions')
452 parser.add_argument("-q", '--quiet',
456 help='be quiet and minimise output')
457 parser.add_argument('input')
458 parser.add_argument('ouput')
460 args = parser.parse_args()
462 convert(args.input, args.output, not options.func_def, options.quiet)
465 if __name__ == '__main__':