3 # Copyright 2015-2016 Thomas Martitz <kugel@rockbox.org>
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
23 from lxml
import etree
24 from optparse
import OptionParser
27 def normalize_text(s
):
29 Normalizes whitespace in text.
31 >>> normalize_text("asd xxx")
33 >>> normalize_text(" asd\nxxx ")
36 return s
.replace("\n", " ").strip()
39 CXX_NAMESPACE_RE
= re
.compile(r
'[_a-zA-Z][_0-9a-zA-Z]*::')
40 def fix_definition(s
):
42 Removes C++ name qualifications from some definitions.
46 >>> fix_definition("bool flag")
48 >>> fix_definition("bool FooBar::flag")
50 >>> fix_definition("void(* _GeanyObjectClass::project_open) (GKeyFile *keyfile)")
51 'void(* project_open) (GKeyFile *keyfile)'
54 return CXX_NAMESPACE_RE
.sub(r
"", s
)
63 def cb(self
, type, str):
65 words
= str.split(" ", 2)
67 elif (type == "return"):
69 elif (type == "since"):
70 self
.since
= str.rstrip()
71 elif type in ("geany:nullable",
77 self
.annot
.append(type.split(":")[1])
78 elif type in ("geany:transfer",
81 type = type.split(":")[1]
82 self
.annot
.append("%s %s" % (type, str))
85 elif type in ("a", "c") and str in ("NULL", "TRUE", "FALSE"):
86 # FIXME: some of Geany does @a NULL instead of @c NULL
96 class DoxygenProcess(object):
100 # http://stackoverflow.com/questions/4624062/get-all-text-inside-a-tag-in-lxml
102 def stringify_children(node
):
103 from lxml
.etree
import tostring
104 from itertools
import chain
105 parts
= ([node
.text
] +
106 list(chain(*([c
.text
, tostring(c
).decode("utf-8"), c
.tail
] for c
in node
.getchildren()))) +
108 # filter removes possible Nones in texts and tails
109 return "".join(filter(None, parts
))
111 def get_program_listing(self
, xml
):
112 from lxml
.etree
import tostring
113 arr
= ["", "|[<!-- language=\"C\" -->"]
114 for l
in xml
.getchildren():
115 if (l
.tag
== "codeline"):
116 # a codeline is of the form
117 # <highlight class="normal">GeanyDocument<sp/>*doc<sp/>=<sp/>...;</highlight>
118 # <sp/> tags must be replaced with spaces, then just use the text
119 h
= l
.find("highlight")
121 html
= tostring(h
).decode("utf-8")
122 html
= html
.replace("<sp/>", " ")
123 arr
.append(" " + tostring(etree
.HTML(html
), method
="text").decode("utf-8"))
125 return "\n".join(arr
)
127 def join_annot(self
):
128 s
= " ".join(map(lambda x
: "(%s)" % x
, self
.at
.annot
))
129 return s
+ ": " if s
else ""
131 def process_element(self
, xml
):
133 s
= self
.__process
_element
(xml
)
137 return self
.join_annot()
139 def get_return(self
):
140 return self
.at
.retval
145 def __process_element(self
, xml
):
150 for n
in xml
.getchildren():
151 if n
.tag
== "emphasis":
152 s
+= self
.at
.cb("a", self
.__process
_element
(n
))
153 if n
.tag
== "computeroutput":
154 s
+= self
.at
.cb("c", self
.__process
_element
(n
))
155 if n
.tag
== "itemizedlist":
156 s
+= "\n" + self
.__process
_element
(n
)
157 if n
.tag
== "listitem":
158 s
+= " - " + self
.__process
_element
(n
)
160 s
+= self
.__process
_element
(n
) + "\n"
162 s
+= n
.text
if n
.text
else ""
163 if n
.tag
== "simplesect":
164 ss
= self
.at
.cb(n
.get("kind"), self
.__process
_element
(n
))
165 s
+= ss
+ "\n" if ss
else ""
166 if n
.tag
== "programlisting":
167 s
+= self
.get_program_listing(n
)
168 if n
.tag
== "xrefsect":
169 s
+= self
.__process
_element
(n
)
170 if n
.tag
== "xreftitle":
171 s
+= self
.__process
_element
(n
) + ": "
172 if n
.tag
== "xrefdescription":
173 s
+= self
.__process
_element
(n
)
175 s
+= self
.__process
_element
(n
)
176 if n
.tag
== "linebreak":
180 # workaround for doxygen bug #646002
181 if n
.tag
== "htmlonly":
185 if n
.tag
.startswith("param"):
186 pass # parameters are handled separately in DoxyFunction::from_memberdef()
190 class DoxyMember(object):
191 def __init__(self
, name
, brief
, extra
=""):
197 class DoxyElement(object):
199 def __init__(self
, name
, definition
, **kwargs
):
201 self
.definition
= definition
202 self
.brief
= kwargs
.get('brief', "")
203 self
.detail
= kwargs
.get('detail', "")
204 self
.members
= kwargs
.get('members', [])
205 self
.since
= kwargs
.get('since', "")
206 self
.extra
= kwargs
.get('extra', "")
207 self
.retval
= kwargs
.get('retval', None)
209 def is_documented(self
):
210 if (normalize_text(self
.brief
)) != "":
214 def add_brief(self
, xml
):
215 proc
= DoxygenProcess()
216 self
.brief
= proc
.process_element(xml
)
217 self
.extra
+= proc
.get_extra()
219 def add_detail(self
, xml
):
220 proc
= DoxygenProcess()
221 self
.detail
= proc
.process_element(xml
)
222 self
.extra
+= proc
.get_extra()
223 self
.since
= proc
.get_since()
225 def add_member(self
, xml
):
226 name
= xml
.find("name").text
227 proc
= DoxygenProcess()
228 brief
= proc
.process_element(xml
.find("briefdescription"))
229 # optional doxygen command output appears within <detaileddescription />
230 proc
.process_element(xml
.find("detaileddescription"))
231 self
.members
.append(DoxyMember(name
, normalize_text(brief
), proc
.get_extra()))
233 def add_param(self
, xml
):
234 name
= xml
.find("parameternamelist").find("parametername").text
235 proc
= DoxygenProcess()
236 brief
= proc
.process_element(xml
.find("parameterdescription"))
237 self
.members
.append(DoxyMember(name
, normalize_text(brief
), proc
.get_extra()))
239 def add_return(self
, xml
):
240 proc
= DoxygenProcess()
241 brief
= proc
.process_element(xml
)
242 self
.retval
= DoxyMember("ret", normalize_text(brief
), proc
.get_extra())
247 s
.append(" * %s: %s" % (self
.name
, self
.extra
))
248 for p
in self
.members
:
249 s
.append(" * @%s: %s %s" % (p
.name
, p
.extra
, p
.brief
))
251 s
.append(" * %s" % self
.brief
.replace("\n", "\n * "))
253 s
.append(" * %s" % self
.detail
.replace("\n", "\n * "))
256 s
.append(" * Returns: %s %s" % (self
.retval
.extra
, self
.retval
.brief
))
259 s
.append(" * Since: %s" % self
.since
)
265 class DoxyTypedef(DoxyElement
):
267 def from_memberdef(xml
):
268 name
= xml
.find("name").text
269 d
= normalize_text(xml
.find("definition").text
)
271 return DoxyTypedef(name
, d
)
274 class DoxyEnum(DoxyElement
):
276 def from_memberdef(xml
):
277 name
= xml
.find("name").text
278 d
= "typedef enum {\n"
279 for member
in xml
.findall("enumvalue"):
280 v
= member
.find("initializer")
281 d
+= "\t%s%s,\n" % (member
.find("name").text
, " "+v
.text
if v
is not None else "")
282 d
+= "} %s;\n" % name
284 e
= DoxyEnum(name
, d
)
285 e
.add_brief(xml
.find("briefdescription"))
286 for p
in xml
.findall("enumvalue"):
291 class DoxyStruct(DoxyElement
):
293 def from_compounddef(xml
, typedefs
=[]):
294 name
= xml
.find("compoundname").text
295 d
= "struct %s {\n" % name
296 memberdefs
= xml
.xpath(".//sectiondef[@kind='public-attrib']/memberdef")
298 # workaround for struct members. g-ir-scanner can't properly map struct members
299 # (beginning with struct GeanyFoo) to the typedef and assigns a generic type for them
300 # thus we fix that up here and enforce usage of the typedef. These are written
301 # out first, before any struct definition, for this reason
302 # Exception: there are no typedefs for GeanyFooPrivate so skip those. Their exact
303 # type isn't needed anyway
304 s
= fix_definition(p
.find("definition").text
).lstrip()
305 proc
= DoxygenProcess()
306 brief
= proc
.process_element(p
.find("briefdescription"))
307 private
= (normalize_text(brief
) == "")
309 if (words
[0] == "struct"):
310 if not (words
[1].endswith("Private") or words
[1].endswith("Private*")):
311 s
= " ".join(words
[1:])
312 d
+= "\t/*< %s >*/\n\t%s;\n" % ("private" if private
else "public", s
)
315 e
= DoxyStruct(name
, d
)
316 e
.add_brief(xml
.find("briefdescription"))
322 class DoxyFunction(DoxyElement
):
324 def from_memberdef(xml
):
325 name
= xml
.find("name").text
326 d
= normalize_text(xml
.find("definition").text
)
327 d
+= " " + xml
.find("argsstring").text
+ ";"
328 d
= normalize_text(d
)
330 e
= DoxyFunction(name
, d
)
331 e
.add_brief(xml
.find("briefdescription"))
332 e
.add_detail(xml
.find("detaileddescription"))
333 for p
in xml
.xpath(".//detaileddescription/*/parameterlist[@kind='param']/parameteritem"):
335 x
= xml
.xpath(".//detaileddescription/*/simplesect[@kind='return']")
346 parser
= OptionParser(usage
="usage: %prog [options] XML_DIR")
347 parser
.add_option("--xmldir", metavar
="DIRECTORY", help="Path to Doxygen-generated XML files",
348 action
="store", dest
="xml_dir")
349 parser
.add_option("-d", "--outdir", metavar
="DIRECTORY", help="Path to Doxygen-generated XML files",
350 action
="store", dest
="outdir", default
=".")
351 parser
.add_option("-o", "--output", metavar
="FILE", help="Write output to FILE",
352 action
="store", dest
="outfile")
353 parser
.add_option("--sci-output", metavar
="FILE", help="Write output to FILE (only sciwrappers)",
354 action
="store", dest
="scioutfile")
355 opts
, args
= parser
.parse_args(args
[1:])
359 if not (os
.path
.exists(xml_dir
)):
360 sys
.stderr
.write("invalid xml directory\n")
363 transform
= etree
.XSLT(etree
.parse(os
.path
.join(xml_dir
, "combine.xslt")))
364 doc
= etree
.parse(os
.path
.join(xml_dir
, "index.xml"))
365 root
= transform(doc
)
371 c_files
= root
.xpath(".//compounddef[@kind='file']/compoundname[substring(.,string-length(.)-1)='.c']/..")
372 h_files
= root
.xpath(".//compounddef[@kind='file']/compoundname[substring(.,string-length(.)-1)='.h']/..")
375 if not (f
.find("compoundname").text
.endswith("private.h")):
376 for n0
in f
.xpath(".//*/memberdef[@kind='typedef' and @prot='public']"):
377 if not (DoxygenProcess
.stringify_children(n0
.find("type")).startswith("enum")):
378 e
= DoxyTypedef
.from_memberdef(n0
)
381 for n0
in f
.xpath(".//*/memberdef[@kind='enum' and @prot='public']"):
382 e
= DoxyEnum
.from_memberdef(n0
)
385 for n0
in root
.xpath(".//compounddef[@kind='struct' and @prot='public']"):
386 e
= DoxyStruct
.from_compounddef(n0
)
390 for n0
in f
.xpath(".//*/memberdef[@kind='function' and @prot='public']"):
391 e
= DoxyFunction
.from_memberdef(n0
)
396 outfile
= open(opts
.outfile
, "w+")
397 except OSError as err
:
398 sys
.stderr
.write("failed to open \"%s\" for writing (%s)\n" % (opts
.outfile
, err
.strerror
))
403 if (opts
.scioutfile
):
405 scioutfile
= open(opts
.scioutfile
, "w+")
406 except OSError as err
:
407 sys
.stderr
.write("failed to open \"%s\" for writing (%s)\n" % (opts
.scioutfile
, err
.strerror
))
413 outfile
.write("/*\n * Automatically generated file - do not edit\n */\n\n")
414 outfile
.write("#include \"gtkcompat.h\"\n")
415 outfile
.write("#include \"Scintilla.h\"\n")
416 outfile
.write("#include \"ScintillaWidget.h\"\n")
417 if (scioutfile
!= outfile
):
418 scioutfile
.write("/*\n * Automatically generated file - do not edit\n */\n\n")
419 scioutfile
.write("#include \"gtkcompat.h\"\n")
420 scioutfile
.write("#include \"Scintilla.h\"\n")
421 scioutfile
.write("#include \"ScintillaWidget.h\"\n")
423 # write enums first, so typedefs to them are valid (as forward enum declaration
424 # is invalid). It's fine as an enum can't contain reference to other types.
425 for e
in filter(lambda x
: x
.is_documented(), enums
):
426 outfile
.write("\n\n")
427 outfile
.write(e
.to_gtkdoc())
428 outfile
.write(e
.definition
)
429 outfile
.write("\n\n")
431 # write typedefs second, they are possibly undocumented but still required (even
432 # if they are documented, they must be written out without gtkdoc)
434 outfile
.write(e
.definition
)
435 outfile
.write("\n\n")
437 # write the rest (structures, functions, ...)
438 for e
in filter(lambda x
: x
.is_documented(), other
):
439 outfile
.write("\n\n")
440 outfile
.write(e
.to_gtkdoc())
441 outfile
.write(e
.definition
)
442 outfile
.write("\n\n")
443 if (e
.name
.startswith("sci_")):
444 if (scioutfile
!= outfile
):
445 scioutfile
.write("\n\n")
446 scioutfile
.write(e
.to_gtkdoc())
447 scioutfile
.write(e
.definition
)
448 scioutfile
.write("\n\n")
450 except BrokenPipeError
:
451 # probably piped to head or tail
456 if __name__
== "__main__":
457 sys
.exit(main(sys
.argv
))