makedocs.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. #
  4. # makedocs.py: Generate documentation for Open Project Wiki
  5. # Copyright (c) 2007-2016 Juan Linietsky, Ariel Manzur.
  6. # Contributor: Jorge Araya Navarro <[email protected]>
  7. #
  8. # IMPORTANT NOTICE:
  9. # If you are going to modify anything from this file, please be sure to follow
  10. # the Style Guide for Python Code or often called "PEP8". To do this
  11. # automagically just install autopep8:
  12. #
  13. # $ sudo pip3 install autopep8
  14. #
  15. # and run:
  16. #
  17. # $ autopep8 makedocs.py
  18. #
  19. # Before committing your changes. Also be sure to delete any trailing
  20. # whitespace you may left.
  21. #
  22. # TODO:
  23. # * Refactor code.
  24. # * Adapt this script for generating content in other markup formats like
  25. # reStructuredText, Markdown, DokuWiki, etc.
  26. #
  27. # Also check other TODO entries in this script for more information on what is
  28. # left to do.
  29. import argparse
  30. import gettext
  31. import logging
  32. import re
  33. from itertools import zip_longest
  34. from os import path, listdir
  35. from xml.etree import ElementTree
  36. # add an option to change the verbosity
  37. logging.basicConfig(level=logging.INFO)
  38. def getxmlfloc():
  39. """ Returns the supposed location of the XML file
  40. """
  41. filepath = path.dirname(path.abspath(__file__))
  42. return path.join(filepath, "class_list.xml")
  43. def langavailable():
  44. """ Return a list of languages available for translation
  45. """
  46. filepath = path.join(
  47. path.dirname(path.abspath(__file__)), "locales")
  48. files = listdir(filepath)
  49. choices = [x for x in files]
  50. choices.insert(0, "none")
  51. return choices
  52. desc = "Generates documentation from a XML file to different markup languages"
  53. parser = argparse.ArgumentParser(description=desc)
  54. parser.add_argument("--input", dest="xmlfp", default=getxmlfloc(),
  55. help="Input XML file, default: {}".format(getxmlfloc()))
  56. parser.add_argument("--output-dir", dest="outputdir", required=True,
  57. help="Output directory for generated files")
  58. parser.add_argument("--language", choices=langavailable(), default="none",
  59. help=("Choose the language of translation"
  60. " for the output files. Default is English (none). "
  61. "Note: This is NOT for the documentation itself!"))
  62. # TODO: add an option for outputting different markup formats
  63. args = parser.parse_args()
  64. # Let's check if the file and output directory exists
  65. if not path.isfile(args.xmlfp):
  66. logging.critical("File not found: {}".format(args.xmlfp))
  67. exit(1)
  68. elif not path.isdir(args.outputdir):
  69. logging.critical("Path does not exist: {}".format(args.outputdir))
  70. exit(1)
  71. _ = gettext.gettext
  72. if args.language != "none":
  73. lang = gettext.translation(domain="makedocs",
  74. localedir="locales",
  75. languages=[args.language])
  76. lang.install()
  77. _ = lang.gettext
  78. # Strings
  79. C_LINK = _("\"<code>{gclass}</code>(Go to page of class"
  80. " {gclass})\":/class_{lkclass}")
  81. MC_LINK = _("\"<code>{gclass}.{method}</code>(Go "
  82. "to page {gclass}, section {method})\""
  83. ":/class_{lkclass}#{lkmethod}")
  84. TM_JUMP = _("\"<code>{method}</code>(Jump to method"
  85. " {method})\":#{lkmethod}")
  86. GTC_LINK = _(" \"{rtype}(Go to page of class {rtype})\":/class_{link} ")
  87. DFN_JUMP = _("\"*{funcname}*(Jump to description for"
  88. " node {funcname})\":#{link} <b>(</b> ")
  89. M_ARG_DEFAULT = C_LINK + " {name}={default}"
  90. M_ARG = C_LINK + " {name}"
  91. OPENPROJ_INH = _("h4. Inherits: ") + C_LINK + "\n\n"
  92. def tb(string):
  93. """ Return a byte representation of a string
  94. """
  95. return bytes(string, "UTF-8")
  96. def sortkey(c):
  97. """ Symbols are first, letters second
  98. """
  99. if "_" == c.attrib["name"][0]:
  100. return "A"
  101. else:
  102. return c.attrib["name"]
  103. def toOP(text):
  104. """ Convert commands in text to Open Project commands
  105. """
  106. # TODO: Make this capture content between [command] ... [/command]
  107. groups = re.finditer((r'\[html (?P<command>/?\w+/?)(\]| |=)?(\]| |=)?(?P<a'
  108. 'rg>\w+)?(\]| |=)?(?P<value>"[^"]+")?/?\]'), text)
  109. alignstr = ""
  110. for group in groups:
  111. gd = group.groupdict()
  112. if gd["command"] == "br/":
  113. text = text.replace(group.group(0), "\n\n", 1)
  114. elif gd["command"] == "div":
  115. if gd["value"] == '"center"':
  116. alignstr = ("{display:block; margin-left:auto;"
  117. " margin-right:auto;}")
  118. elif gd["value"] == '"left"':
  119. alignstr = "<"
  120. elif gd["value"] == '"right"':
  121. alignstr = ">"
  122. text = text.replace(group.group(0), "\n\n", 1)
  123. elif gd["command"] == "/div":
  124. alignstr = ""
  125. text = text.replace(group.group(0), "\n\n", 1)
  126. elif gd["command"] == "img":
  127. text = text.replace(group.group(0), "!{align}{src}!".format(
  128. align=alignstr, src=gd["value"].strip('"')), 1)
  129. elif gd["command"] == "b" or gd["command"] == "/b":
  130. text = text.replace(group.group(0), "*", 1)
  131. elif gd["command"] == "i" or gd["command"] == "/i":
  132. text = text.replace(group.group(0), "_", 1)
  133. elif gd["command"] == "u" or gd["command"] == "/u":
  134. text = text.replace(group.group(0), "+", 1)
  135. # Process other non-html commands
  136. groups = re.finditer((r'\[method ((?P<class>[aA0-zZ9_]+)(?:\.))'
  137. r'?(?P<method>[aA0-zZ9_]+)\]'), text)
  138. for group in groups:
  139. gd = group.groupdict()
  140. if gd["class"]:
  141. replacewith = (MC_LINK.format(gclass=gd["class"],
  142. method=gd["method"],
  143. lkclass=gd["class"].lower(),
  144. lkmethod=gd["method"].lower()))
  145. else:
  146. # The method is located in the same wiki page
  147. replacewith = (TM_JUMP.format(method=gd["method"],
  148. lkmethod=gd["method"].lower()))
  149. text = text.replace(group.group(0), replacewith, 1)
  150. # Finally, [Classes] are around brackets, make them direct links
  151. groups = re.finditer(r'\[(?P<class>[az0-AZ0_]+)\]', text)
  152. for group in groups:
  153. gd = group.groupdict()
  154. replacewith = (C_LINK.
  155. format(gclass=gd["class"],
  156. lkclass=gd["class"].lower()))
  157. text = text.replace(group.group(0), replacewith, 1)
  158. return text + "\n\n"
  159. def mkfn(node, is_signal=False):
  160. """ Return a string containing a unsorted item for a function
  161. """
  162. finalstr = ""
  163. name = node.attrib["name"]
  164. rtype = node.find("return")
  165. if rtype:
  166. rtype = rtype.attrib["type"]
  167. else:
  168. rtype = "void"
  169. # write the return type and the function name first
  170. finalstr += "* "
  171. # return type
  172. if not is_signal:
  173. if rtype != "void":
  174. finalstr += GTC_LINK.format(
  175. rtype=rtype,
  176. link=rtype.lower())
  177. else:
  178. finalstr += " void "
  179. # function name
  180. if not is_signal:
  181. finalstr += DFN_JUMP.format(
  182. funcname=name,
  183. link=name.lower())
  184. else:
  185. # Signals have no description
  186. finalstr += "*{funcname}* <b>(</b>".format(funcname=name)
  187. # loop for the arguments of the function, if any
  188. args = []
  189. for arg in sorted(
  190. node.iter(tag="argument"),
  191. key=lambda a: int(a.attrib["index"])):
  192. ntype = arg.attrib["type"]
  193. nname = arg.attrib["name"]
  194. if "default" in arg.attrib:
  195. args.insert(-1, M_ARG_DEFAULT.format(
  196. gclass=ntype,
  197. lkclass=ntype.lower(),
  198. name=nname,
  199. default=arg.attrib["default"]))
  200. else:
  201. # No default value present
  202. args.insert(-1, M_ARG.format(gclass=ntype,
  203. lkclass=ntype.lower(), name=nname))
  204. # join the arguments together
  205. finalstr += ", ".join(args)
  206. # and, close the function with a )
  207. finalstr += " <b>)</b>"
  208. # write the qualifier, if any
  209. if "qualifiers" in node.attrib:
  210. qualifier = node.attrib["qualifiers"]
  211. finalstr += " " + qualifier
  212. finalstr += "\n"
  213. return finalstr
  214. # Let's begin
  215. tree = ElementTree.parse(args.xmlfp)
  216. root = tree.getroot()
  217. # Check version attribute exists in <doc>
  218. if "version" not in root.attrib:
  219. logging.critical(_("<doc>'s version attribute missing"))
  220. exit(1)
  221. version = root.attrib["version"]
  222. classes = sorted(root, key=sortkey)
  223. # first column is always longer, second column of classes should be shorter
  224. zclasses = zip_longest(classes[:int(len(classes) / 2 + 1)],
  225. classes[int(len(classes) / 2 + 1):],
  226. fillvalue="")
  227. # We write the class_list file and also each class file at once
  228. with open(path.join(args.outputdir, "class_list.txt"), "wb") as fcl:
  229. # Write header of table
  230. fcl.write(tb("|^.\n"))
  231. fcl.write(tb(_("|_. Index symbol |_. Class name "
  232. "|_. Index symbol |_. Class name |\n")))
  233. fcl.write(tb("|-.\n"))
  234. indexletterl = ""
  235. indexletterr = ""
  236. for gdclassl, gdclassr in zclasses:
  237. # write a row #
  238. # write the index symbol column, left
  239. if indexletterl != gdclassl.attrib["name"][0]:
  240. indexletterl = gdclassl.attrib["name"][0]
  241. fcl.write(tb("| *{}* |".format(indexletterl.upper())))
  242. else:
  243. # empty cell
  244. fcl.write(tb("| |"))
  245. # write the class name column, left
  246. fcl.write(tb(C_LINK.format(
  247. gclass=gdclassl.attrib["name"],
  248. lkclass=gdclassl.attrib["name"].lower())))
  249. # write the index symbol column, right
  250. if isinstance(gdclassr, ElementTree.Element):
  251. if indexletterr != gdclassr.attrib["name"][0]:
  252. indexletterr = gdclassr.attrib["name"][0]
  253. fcl.write(tb("| *{}* |".format(indexletterr.upper())))
  254. else:
  255. # empty cell
  256. fcl.write(tb("| |"))
  257. # We are dealing with an empty string
  258. else:
  259. # two empty cell
  260. fcl.write(tb("| | |\n"))
  261. # We won't get the name of the class since there is no ElementTree
  262. # object for the right side of the tuple, so we iterate the next
  263. # tuple instead
  264. continue
  265. # write the class name column (if any), right
  266. fcl.write(tb(C_LINK.format(
  267. gclass=gdclassl.attrib["name"],
  268. lkclass=gdclassl.attrib["name"].lower()) + "|\n"))
  269. # row written #
  270. # now, let's write each class page for each class
  271. for gdclass in [gdclassl, gdclassr]:
  272. if not isinstance(gdclass, ElementTree.Element):
  273. continue
  274. classname = gdclass.attrib["name"]
  275. with open(path.join(args.outputdir, "{}.txt".format(
  276. classname.lower())), "wb") as clsf:
  277. # First level header with the name of the class
  278. clsf.write(tb("h1. {}\n\n".format(classname)))
  279. # lay the attributes
  280. if "inherits" in gdclass.attrib:
  281. inh = gdclass.attrib["inherits"].strip()
  282. clsf.write(tb(OPENPROJ_INH.format(gclass=inh,
  283. lkclass=inh.lower())))
  284. if "category" in gdclass.attrib:
  285. clsf.write(tb(_("h4. Category: {}\n\n").
  286. format(gdclass.attrib["category"].strip())))
  287. # lay child nodes
  288. briefd = gdclass.find("brief_description")
  289. if briefd.text.strip():
  290. clsf.write(tb(_("h2. Brief Description\n\n")))
  291. clsf.write(tb(toOP(briefd.text.strip()) +
  292. _("\"read more\":#more\n\n")))
  293. # Write the list of member functions of this class
  294. methods = gdclass.find("methods")
  295. if methods and len(methods) > 0:
  296. clsf.write(tb(_("\nh3. Member Functions\n\n")))
  297. for method in methods.iter(tag='method'):
  298. clsf.write(tb(mkfn(method)))
  299. signals = gdclass.find("signals")
  300. if signals and len(signals) > 0:
  301. clsf.write(tb(_("\nh3. Signals\n\n")))
  302. for signal in signals.iter(tag='signal'):
  303. clsf.write(tb(mkfn(signal, True)))
  304. # TODO: <members> tag is necessary to process? it does not
  305. # exists in class_list.xml file.
  306. consts = gdclass.find("constants")
  307. if consts and len(consts) > 0:
  308. clsf.write(tb(_("\nh3. Numeric Constants\n\n")))
  309. for const in sorted(consts, key=lambda k:
  310. k.attrib["name"]):
  311. if const.text.strip():
  312. clsf.write(tb("* *{name}* = *{value}* - {desc}\n".
  313. format(
  314. name=const.attrib["name"],
  315. value=const.attrib["value"],
  316. desc=const.text.strip())))
  317. else:
  318. # Constant have no description
  319. clsf.write(tb("* *{name}* = *{value}*\n".
  320. format(
  321. name=const.attrib["name"],
  322. value=const.attrib["value"])))
  323. descrip = gdclass.find("description")
  324. clsf.write(tb(_("\nh3(#more). Description\n\n")))
  325. if descrip.text:
  326. clsf.write(tb(descrip.text.strip() + "\n"))
  327. else:
  328. clsf.write(tb(_("_Nothing here, yet..._\n")))
  329. # and finally, the description for each method
  330. if methods and len(methods) > 0:
  331. clsf.write(tb(_("\nh3. Member Function Description\n\n")))
  332. for method in methods.iter(tag='method'):
  333. clsf.write(tb("h4(#{n}). {name}\n\n".format(
  334. n=method.attrib["name"].lower(),
  335. name=method.attrib["name"])))
  336. clsf.write(tb(mkfn(method) + "\n"))
  337. clsf.write(tb(toOP(method.find(
  338. "description").text.strip())))