3 # Copyright 2015 The Geany contributors
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:array",
82 type = type.split(":")[1]
85 self
.annot
.append("%s%s" % (type, str))
88 elif type in ("a", "c") and str in ("NULL", "TRUE", "FALSE"):
89 # FIXME: some of Geany does @a NULL instead of @c NULL
99 class DoxygenProcess(object):
103 # http://stackoverflow.com/questions/4624062/get-all-text-inside-a-tag-in-lxml
105 def stringify_children(node
):
106 from lxml
.etree
import tostring
107 from itertools
import chain
108 parts
= ([node
.text
] +
109 list(chain(*([c
.text
, tostring(c
, encoding
='unicode'), c
.tail
] for c
in node
.getchildren()))) +
111 # filter removes possible Nones in texts and tails
112 return "".join(filter(None, parts
))
114 def get_program_listing(self
, xml
):
115 from lxml
.etree
import tostring
116 arr
= ["", "|[<!-- language=\"C\" -->"]
117 for l
in xml
.getchildren():
118 if (l
.tag
== "codeline"):
119 # a codeline is of the form
120 # <highlight class="normal">GeanyDocument<sp/>*doc<sp/>=<sp/>...;</highlight>
121 # <sp/> tags must be replaced with spaces, then just use the text
122 h
= l
.find("highlight")
124 html
= tostring(h
, encoding
='unicode')
125 html
= html
.replace("<sp/>", " ")
126 arr
.append(" " + tostring(etree
.HTML(html
), method
="text", encoding
='unicode'))
128 return "\n".join(arr
)
130 def join_annot(self
):
131 s
= " ".join(map(lambda x
: "(%s)" % x
, self
.at
.annot
))
132 return s
+ ": " if s
else ""
134 def process_element(self
, xml
):
136 s
= self
.__process
_element
(xml
)
140 return self
.join_annot()
142 def get_return(self
):
143 return self
.at
.retval
148 def __process_element(self
, xml
):
153 for n
in xml
.getchildren():
154 if n
.tag
== "emphasis":
155 s
+= self
.at
.cb("a", self
.__process
_element
(n
))
156 if n
.tag
== "computeroutput":
157 s
+= self
.at
.cb("c", self
.__process
_element
(n
))
158 if n
.tag
== "itemizedlist":
159 s
+= "\n" + self
.__process
_element
(n
)
160 if n
.tag
== "listitem":
161 s
+= " - " + self
.__process
_element
(n
)
163 s
+= self
.__process
_element
(n
) + "\n"
165 s
+= n
.text
if n
.text
else ""
166 if n
.tag
== "simplesect":
167 ss
= self
.at
.cb(n
.get("kind"), self
.__process
_element
(n
))
168 s
+= ss
+ "\n" if ss
else ""
169 if n
.tag
== "programlisting":
170 s
+= self
.get_program_listing(n
)
171 if n
.tag
== "xrefsect":
172 s
+= self
.__process
_element
(n
)
173 if n
.tag
== "xreftitle":
174 s
+= self
.__process
_element
(n
) + ": "
175 if n
.tag
== "xrefdescription":
176 s
+= self
.__process
_element
(n
)
178 s
+= self
.__process
_element
(n
)
179 if n
.tag
== "linebreak":
183 # workaround for doxygen bug #646002
184 if n
.tag
== "htmlonly":
188 if n
.tag
.startswith("param"):
189 pass # parameters are handled separately in DoxyFunction::from_memberdef()
193 class DoxyMember(object):
194 def __init__(self
, name
, brief
, extra
=""):
200 class DoxyElement(object):
202 def __init__(self
, name
, definition
, **kwargs
):
204 self
.definition
= definition
205 self
.brief
= kwargs
.get('brief', "")
206 self
.detail
= kwargs
.get('detail', "")
207 self
.members
= kwargs
.get('members', [])
208 self
.since
= kwargs
.get('since', "")
209 self
.extra
= kwargs
.get('extra', "")
210 self
.retval
= kwargs
.get('retval', None)
212 def is_documented(self
):
213 if (normalize_text(self
.brief
)) != "":
217 def add_brief(self
, xml
):
218 proc
= DoxygenProcess()
219 self
.brief
= proc
.process_element(xml
)
220 self
.extra
+= proc
.get_extra()
222 def add_detail(self
, xml
):
223 proc
= DoxygenProcess()
224 self
.detail
= proc
.process_element(xml
)
225 self
.extra
+= proc
.get_extra()
226 self
.since
= proc
.get_since()
228 def add_member(self
, xml
):
229 name
= xml
.find("name").text
230 proc
= DoxygenProcess()
231 brief
= proc
.process_element(xml
.find("briefdescription"))
232 # optional doxygen command output appears within <detaileddescription />
233 proc
.process_element(xml
.find("detaileddescription"))
234 self
.members
.append(DoxyMember(name
, normalize_text(brief
), proc
.get_extra()))
236 def add_param(self
, xml
):
237 name
= xml
.find("parameternamelist").find("parametername").text
238 proc
= DoxygenProcess()
239 brief
= proc
.process_element(xml
.find("parameterdescription"))
240 self
.members
.append(DoxyMember(name
, normalize_text(brief
), proc
.get_extra()))
242 def add_return(self
, xml
):
243 proc
= DoxygenProcess()
244 brief
= proc
.process_element(xml
)
245 self
.retval
= DoxyMember("ret", normalize_text(brief
), proc
.get_extra())
250 s
.append(" * %s: %s" % (self
.name
, self
.extra
))
251 for p
in self
.members
:
252 s
.append(" * @%s: %s %s" % (p
.name
, p
.extra
, p
.brief
))
254 s
.append(" * %s" % self
.brief
.replace("\n", "\n * "))
256 s
.append(" * %s" % self
.detail
.replace("\n", "\n * "))
259 s
.append(" * Returns: %s %s" % (self
.retval
.extra
, self
.retval
.brief
))
262 s
.append(" * Since: %s" % self
.since
)
268 class DoxyTypedef(DoxyElement
):
270 def from_memberdef(xml
):
271 name
= xml
.find("name").text
272 d
= normalize_text(xml
.find("definition").text
)
274 return DoxyTypedef(name
, d
)
277 class DoxyEnum(DoxyElement
):
279 def from_memberdef(xml
):
280 name
= xml
.find("name").text
281 d
= "typedef enum {\n"
282 for member
in xml
.findall("enumvalue"):
283 v
= member
.find("initializer")
284 d
+= "\t%s%s,\n" % (member
.find("name").text
, " "+v
.text
if v
is not None else "")
285 d
+= "} %s;\n" % name
287 e
= DoxyEnum(name
, d
)
288 e
.add_brief(xml
.find("briefdescription"))
289 for p
in xml
.findall("enumvalue"):
294 class DoxyStruct(DoxyElement
):
296 def from_compounddef(xml
, typedefs
=[]):
297 name
= xml
.find("compoundname").text
298 d
= "struct %s {\n" % name
299 memberdefs
= xml
.xpath(".//sectiondef[@kind='public-attrib']/memberdef")
301 # workaround for struct members. g-ir-scanner can't properly map struct members
302 # (beginning with struct GeanyFoo) to the typedef and assigns a generic type for them
303 # thus we fix that up here and enforce usage of the typedef. These are written
304 # out first, before any struct definition, for this reason
305 # Exception: there are no typedefs for GeanyFooPrivate so skip those. Their exact
306 # type isn't needed anyway
307 s
= fix_definition(p
.find("definition").text
).lstrip()
308 proc
= DoxygenProcess()
309 brief
= proc
.process_element(p
.find("briefdescription"))
310 private
= (normalize_text(brief
) == "")
312 if (words
[0] == "struct"):
313 if not (words
[1].endswith("Private") or words
[1].endswith("Private*")):
314 s
= " ".join(words
[1:])
315 d
+= "\t/*< %s >*/\n\t%s;\n" % ("private" if private
else "public", s
)
318 e
= DoxyStruct(name
, d
)
319 e
.add_brief(xml
.find("briefdescription"))
325 class DoxyFunction(DoxyElement
):
327 def from_memberdef(xml
):
328 name
= xml
.find("name").text
329 d
= normalize_text(xml
.find("definition").text
)
330 d
+= " " + xml
.find("argsstring").text
+ ";"
331 d
= normalize_text(d
)
333 e
= DoxyFunction(name
, d
)
334 e
.add_brief(xml
.find("briefdescription"))
335 e
.add_detail(xml
.find("detaileddescription"))
336 for p
in xml
.xpath(".//detaileddescription/*/parameterlist[@kind='param']/parameteritem"):
338 x
= xml
.xpath(".//detaileddescription/*/simplesect[@kind='return']")
349 parser
= OptionParser(usage
="usage: %prog [options] XML_DIR")
350 parser
.add_option("--xmldir", metavar
="DIRECTORY", help="Path to Doxygen-generated XML files",
351 action
="store", dest
="xml_dir")
352 parser
.add_option("-d", "--outdir", metavar
="DIRECTORY", help="Path to Doxygen-generated XML files",
353 action
="store", dest
="outdir", default
=".")
354 parser
.add_option("-o", "--output", metavar
="FILE", help="Write output to FILE",
355 action
="store", dest
="outfile")
356 parser
.add_option("--sci-output", metavar
="FILE", help="Write output to FILE (only sciwrappers)",
357 action
="store", dest
="scioutfile")
358 opts
, args
= parser
.parse_args(args
[1:])
362 if not (os
.path
.exists(xml_dir
)):
363 sys
.stderr
.write("invalid xml directory\n")
366 transform
= etree
.XSLT(etree
.parse(os
.path
.join(xml_dir
, "combine.xslt")))
367 doc
= etree
.parse(os
.path
.join(xml_dir
, "index.xml"))
368 root
= transform(doc
)
374 c_files
= root
.xpath(".//compounddef[@kind='file']/compoundname[substring(.,string-length(.)-1)='.c']/..")
375 h_files
= root
.xpath(".//compounddef[@kind='file']/compoundname[substring(.,string-length(.)-1)='.h']/..")
378 if not (f
.find("compoundname").text
.endswith("private.h")):
379 for n0
in f
.xpath(".//*/memberdef[@kind='typedef' and @prot='public']"):
380 if not (DoxygenProcess
.stringify_children(n0
.find("type")).startswith("enum")):
381 e
= DoxyTypedef
.from_memberdef(n0
)
384 for n0
in f
.xpath(".//*/memberdef[@kind='enum' and @prot='public']"):
385 e
= DoxyEnum
.from_memberdef(n0
)
388 for n0
in root
.xpath(".//compounddef[@kind='struct' and @prot='public']"):
389 e
= DoxyStruct
.from_compounddef(n0
)
393 for n0
in f
.xpath(".//*/memberdef[@kind='function' and @prot='public']"):
394 e
= DoxyFunction
.from_memberdef(n0
)
399 outfile
= open(opts
.outfile
, "w+")
400 except OSError as err
:
401 sys
.stderr
.write("failed to open \"%s\" for writing (%s)\n" % (opts
.outfile
, err
.strerror
))
406 if (opts
.scioutfile
):
408 scioutfile
= open(opts
.scioutfile
, "w+")
409 except OSError as err
:
410 sys
.stderr
.write("failed to open \"%s\" for writing (%s)\n" % (opts
.scioutfile
, err
.strerror
))
416 outfile
.write("/*\n * Automatically generated file - do not edit\n */\n\n")
417 outfile
.write("#include \"gtkcompat.h\"\n")
418 outfile
.write("#include \"Scintilla.h\"\n")
419 outfile
.write("#include \"ScintillaWidget.h\"\n")
420 if (scioutfile
!= outfile
):
421 scioutfile
.write("/*\n * Automatically generated file - do not edit\n */\n\n")
422 scioutfile
.write("#include \"gtkcompat.h\"\n")
423 scioutfile
.write("#include \"Scintilla.h\"\n")
424 scioutfile
.write("#include \"ScintillaWidget.h\"\n")
426 # write enums first, so typedefs to them are valid (as forward enum declaration
427 # is invalid). It's fine as an enum can't contain reference to other types.
428 for e
in filter(lambda x
: x
.is_documented(), enums
):
429 outfile
.write("\n\n")
430 outfile
.write(e
.to_gtkdoc())
431 outfile
.write(e
.definition
)
432 outfile
.write("\n\n")
434 # write typedefs second, they are possibly undocumented but still required (even
435 # if they are documented, they must be written out without gtkdoc)
437 outfile
.write(e
.definition
)
438 outfile
.write("\n\n")
440 # write the rest (structures, functions, ...)
441 for e
in filter(lambda x
: x
.is_documented(), other
):
442 outfile
.write("\n\n")
443 outfile
.write(e
.to_gtkdoc())
444 outfile
.write(e
.definition
)
445 outfile
.write("\n\n")
446 if (e
.name
.startswith("sci_")):
447 if (scioutfile
!= outfile
):
448 scioutfile
.write("\n\n")
449 scioutfile
.write(e
.to_gtkdoc())
450 scioutfile
.write(e
.definition
)
451 scioutfile
.write("\n\n")
453 except BrokenPipeError
:
454 # probably piped to head or tail
459 if __name__
== "__main__":
460 sys
.exit(main(sys
.argv
))