makerst.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. import codecs
  4. import sys
  5. import os
  6. import xml.etree.ElementTree as ET
  7. input_list = []
  8. for arg in sys.argv[1:]:
  9. if arg.endswith(os.sep):
  10. arg = arg[:-1]
  11. input_list.append(arg)
  12. if len(input_list) < 1:
  13. print('usage: makerst.py <path to folders> and/or <path to .xml files> (order of arguments irrelevant)')
  14. print('example: makerst.py "../../modules/" "../classes" path_to/some_class.xml')
  15. sys.exit(0)
  16. def validate_tag(elem, tag):
  17. if elem.tag != tag:
  18. print("Tag mismatch, expected '" + tag + "', got " + elem.tag)
  19. sys.exit(255)
  20. class_names = []
  21. classes = {}
  22. def ul_string(str, ul):
  23. str += "\n"
  24. for i in range(len(str) - 1):
  25. str += ul
  26. str += "\n"
  27. return str
  28. def make_class_list(class_list, columns):
  29. f = codecs.open('class_list.rst', 'wb', 'utf-8')
  30. prev = 0
  31. col_max = len(class_list) / columns + 1
  32. print(('col max is ', col_max))
  33. col_count = 0
  34. row_count = 0
  35. last_initial = ''
  36. fit_columns = []
  37. for n in range(0, columns):
  38. fit_columns += [[]]
  39. indexers = []
  40. last_initial = ''
  41. idx = 0
  42. for n in class_list:
  43. col = idx / col_max
  44. if col >= columns:
  45. col = columns - 1
  46. fit_columns[col] += [n]
  47. idx += 1
  48. if n[:1] != last_initial:
  49. indexers += [n]
  50. last_initial = n[:1]
  51. row_max = 0
  52. f.write("\n")
  53. for n in range(0, columns):
  54. if len(fit_columns[n]) > row_max:
  55. row_max = len(fit_columns[n])
  56. f.write("| ")
  57. for n in range(0, columns):
  58. f.write(" | |")
  59. f.write("\n")
  60. f.write("+")
  61. for n in range(0, columns):
  62. f.write("--+-------+")
  63. f.write("\n")
  64. for r in range(0, row_max):
  65. s = '+ '
  66. for c in range(0, columns):
  67. if r >= len(fit_columns[c]):
  68. continue
  69. classname = fit_columns[c][r]
  70. initial = classname[0]
  71. if classname in indexers:
  72. s += '**' + initial + '** | '
  73. else:
  74. s += ' | '
  75. s += '[' + classname + '](class_' + classname.lower() + ') | '
  76. s += '\n'
  77. f.write(s)
  78. for n in range(0, columns):
  79. f.write("--+-------+")
  80. f.write("\n")
  81. def rstize_text(text, cclass):
  82. # Linebreak + tabs in the XML should become two line breaks unless in a "codeblock"
  83. pos = 0
  84. while True:
  85. pos = text.find('\n', pos)
  86. if pos == -1:
  87. break
  88. pre_text = text[:pos]
  89. while text[pos + 1] == '\t':
  90. pos += 1
  91. post_text = text[pos + 1:]
  92. # Handle codeblocks
  93. if post_text.startswith("[codeblock]"):
  94. end_pos = post_text.find("[/codeblock]")
  95. if end_pos == -1:
  96. sys.exit("ERROR! [codeblock] without a closing tag!")
  97. code_text = post_text[len("[codeblock]"):end_pos]
  98. post_text = post_text[end_pos:]
  99. # Remove extraneous tabs
  100. code_pos = 0
  101. while True:
  102. code_pos = code_text.find('\n', code_pos)
  103. if code_pos == -1:
  104. break
  105. to_skip = 0
  106. while code_pos + to_skip + 1 < len(code_text) and code_text[code_pos + to_skip + 1] == '\t':
  107. to_skip += 1
  108. if len(code_text[code_pos + to_skip + 1:]) == 0:
  109. code_text = code_text[:code_pos] + "\n"
  110. code_pos += 1
  111. else:
  112. code_text = code_text[:code_pos] + "\n " + code_text[code_pos + to_skip + 1:]
  113. code_pos += 5 - to_skip
  114. text = pre_text + "\n[codeblock]" + code_text + post_text
  115. pos += len("\n[codeblock]" + code_text)
  116. # Handle normal text
  117. else:
  118. text = pre_text + "\n\n" + post_text
  119. pos += 2
  120. # Escape * character to avoid interpreting it as emphasis
  121. pos = 0
  122. while True:
  123. pos = text.find('*', pos)
  124. if pos == -1:
  125. break
  126. text = text[:pos] + "\*" + text[pos + 1:]
  127. pos += 2
  128. # Escape _ character at the end of a word to avoid interpreting it as an inline hyperlink
  129. pos = 0
  130. while True:
  131. pos = text.find('_', pos)
  132. if pos == -1:
  133. break
  134. if not text[pos + 1].isalnum(): # don't escape within a snake_case word
  135. text = text[:pos] + "\_" + text[pos + 1:]
  136. pos += 2
  137. else:
  138. pos += 1
  139. # Handle [tags]
  140. inside_code = False
  141. pos = 0
  142. while True:
  143. pos = text.find('[', pos)
  144. if pos == -1:
  145. break
  146. endq_pos = text.find(']', pos + 1)
  147. if endq_pos == -1:
  148. break
  149. pre_text = text[:pos]
  150. post_text = text[endq_pos + 1:]
  151. tag_text = text[pos + 1:endq_pos]
  152. if tag_text in class_names:
  153. tag_text = make_type(tag_text)
  154. else: # command
  155. cmd = tag_text
  156. space_pos = tag_text.find(' ')
  157. if cmd == '/codeblock':
  158. tag_text = ''
  159. inside_code = False
  160. # Strip newline if the tag was alone on one
  161. if pre_text[-1] == '\n':
  162. pre_text = pre_text[:-1]
  163. elif cmd == '/code':
  164. tag_text = '``'
  165. inside_code = False
  166. elif inside_code:
  167. tag_text = '[' + tag_text + ']'
  168. elif cmd.find('html') == 0:
  169. cmd = tag_text[:space_pos]
  170. param = tag_text[space_pos + 1:]
  171. tag_text = param
  172. elif cmd.find('method') == 0:
  173. cmd = tag_text[:space_pos]
  174. param = tag_text[space_pos + 1:]
  175. if param.find('.') != -1:
  176. (class_param, method_param) = param.split('.')
  177. tag_text = ':ref:`' + class_param + '.' + method_param + '<class_' + class_param + '_' + method_param + '>`'
  178. else:
  179. tag_text = ':ref:`' + param + '<class_' + cclass + "_" + param + '>`'
  180. elif cmd.find('image=') == 0:
  181. tag_text = "" # '![](' + cmd[6:] + ')'
  182. elif cmd.find('url=') == 0:
  183. tag_text = ':ref:`' + cmd[4:] + '<' + cmd[4:] + ">`"
  184. elif cmd == '/url':
  185. tag_text = ')'
  186. elif cmd == 'center':
  187. tag_text = ''
  188. elif cmd == '/center':
  189. tag_text = ''
  190. elif cmd == 'codeblock':
  191. tag_text = '\n::\n'
  192. inside_code = True
  193. elif cmd == 'br':
  194. # Make a new paragraph instead of a linebreak, rst is not so linebreak friendly
  195. tag_text = '\n\n'
  196. # Strip potential leading spaces
  197. while post_text[0] == ' ':
  198. post_text = post_text[1:]
  199. elif cmd == 'i' or cmd == '/i':
  200. tag_text = '*'
  201. elif cmd == 'b' or cmd == '/b':
  202. tag_text = '**'
  203. elif cmd == 'u' or cmd == '/u':
  204. tag_text = ''
  205. elif cmd == 'code':
  206. tag_text = '``'
  207. inside_code = True
  208. else:
  209. tag_text = make_type(tag_text)
  210. text = pre_text + tag_text + post_text
  211. pos = len(pre_text) + len(tag_text)
  212. return text
  213. def make_type(t):
  214. global class_names
  215. if t in class_names:
  216. return ':ref:`' + t + '<class_' + t.lower() + '>`'
  217. return t
  218. def make_method(
  219. f,
  220. name,
  221. m,
  222. declare,
  223. cname,
  224. event=False,
  225. pp=None
  226. ):
  227. if (declare or pp == None):
  228. t = '- '
  229. else:
  230. t = ""
  231. ret_type = 'void'
  232. args = list(m)
  233. mdata = {}
  234. mdata['argidx'] = []
  235. for a in args:
  236. if a.tag == 'return':
  237. idx = -1
  238. elif a.tag == 'argument':
  239. idx = int(a.attrib['index'])
  240. else:
  241. continue
  242. mdata['argidx'].append(idx)
  243. mdata[idx] = a
  244. if not event:
  245. if -1 in mdata['argidx']:
  246. t += make_type(mdata[-1].attrib['type'])
  247. else:
  248. t += 'void'
  249. t += ' '
  250. if declare or pp == None:
  251. s = ' **' + m.attrib['name'] + '** '
  252. else:
  253. s = ':ref:`' + m.attrib['name'] + '<class_' + cname + "_" + m.attrib['name'] + '>` '
  254. s += ' **(**'
  255. argfound = False
  256. for a in mdata['argidx']:
  257. arg = mdata[a]
  258. if a < 0:
  259. continue
  260. if a > 0:
  261. s += ', '
  262. else:
  263. s += ' '
  264. s += make_type(arg.attrib['type'])
  265. if 'name' in arg.attrib:
  266. s += ' ' + arg.attrib['name']
  267. else:
  268. s += ' arg' + str(a)
  269. if 'default' in arg.attrib:
  270. s += '=' + arg.attrib['default']
  271. argfound = True
  272. if argfound:
  273. s += ' '
  274. s += ' **)**'
  275. if 'qualifiers' in m.attrib:
  276. s += ' ' + m.attrib['qualifiers']
  277. if (not declare):
  278. if (pp != None):
  279. pp.append((t, s))
  280. else:
  281. f.write("- " + t + " " + s + "\n")
  282. else:
  283. f.write(t + s + "\n")
  284. def make_heading(title, underline):
  285. return title + '\n' + underline * len(title) + "\n\n"
  286. def make_rst_class(node):
  287. name = node.attrib['name']
  288. f = codecs.open("class_" + name.lower() + '.rst', 'wb', 'utf-8')
  289. # Warn contributors not to edit this file directly
  290. f.write(".. Generated automatically by doc/tools/makerst.py in Godot's source tree.\n")
  291. f.write(".. DO NOT EDIT THIS FILE, but the " + name + ".xml source instead.\n")
  292. f.write(".. The source is found in doc/classes or modules/<name>/doc_classes.\n\n")
  293. f.write(".. _class_" + name + ":\n\n")
  294. f.write(make_heading(name, '='))
  295. if 'inherits' in node.attrib:
  296. inh = node.attrib['inherits'].strip()
  297. f.write('**Inherits:** ')
  298. first = True
  299. while (inh in classes):
  300. if (not first):
  301. f.write(" **<** ")
  302. else:
  303. first = False
  304. f.write(make_type(inh))
  305. inode = classes[inh]
  306. if ('inherits' in inode.attrib):
  307. inh = inode.attrib['inherits'].strip()
  308. else:
  309. inh = None
  310. f.write("\n\n")
  311. inherited = []
  312. for cn in classes:
  313. c = classes[cn]
  314. if 'inherits' in c.attrib:
  315. if (c.attrib['inherits'].strip() == name):
  316. inherited.append(c.attrib['name'])
  317. if (len(inherited)):
  318. f.write('**Inherited By:** ')
  319. for i in range(len(inherited)):
  320. if (i > 0):
  321. f.write(", ")
  322. f.write(make_type(inherited[i]))
  323. f.write("\n\n")
  324. if 'category' in node.attrib:
  325. f.write('**Category:** ' + node.attrib['category'].strip() + "\n\n")
  326. f.write(make_heading('Brief Description', '-'))
  327. briefd = node.find('brief_description')
  328. if briefd != None:
  329. f.write(rstize_text(briefd.text.strip(), name) + "\n\n")
  330. methods = node.find('methods')
  331. if methods != None and len(list(methods)) > 0:
  332. f.write(make_heading('Member Functions', '-'))
  333. ml = []
  334. for m in list(methods):
  335. make_method(f, node.attrib['name'], m, False, name, False, ml)
  336. longest_t = 0
  337. longest_s = 0
  338. for s in ml:
  339. sl = len(s[0])
  340. if (sl > longest_s):
  341. longest_s = sl
  342. tl = len(s[1])
  343. if (tl > longest_t):
  344. longest_t = tl
  345. sep = "+"
  346. for i in range(longest_s + 2):
  347. sep += "-"
  348. sep += "+"
  349. for i in range(longest_t + 2):
  350. sep += "-"
  351. sep += "+\n"
  352. f.write(sep)
  353. for s in ml:
  354. rt = s[0]
  355. while (len(rt) < longest_s):
  356. rt += " "
  357. st = s[1]
  358. while (len(st) < longest_t):
  359. st += " "
  360. f.write("| " + rt + " | " + st + " |\n")
  361. f.write(sep)
  362. f.write('\n')
  363. events = node.find('signals')
  364. if events != None and len(list(events)) > 0:
  365. f.write(make_heading('Signals', '-'))
  366. for m in list(events):
  367. make_method(f, node.attrib['name'], m, True, name, True)
  368. f.write('\n')
  369. d = m.find('description')
  370. if d == None or d.text.strip() == '':
  371. continue
  372. f.write(rstize_text(d.text.strip(), name))
  373. f.write("\n\n")
  374. f.write('\n')
  375. members = node.find('members')
  376. if members != None and len(list(members)) > 0:
  377. f.write(make_heading('Member Variables', '-'))
  378. for c in list(members):
  379. s = '- '
  380. s += make_type(c.attrib['type']) + ' '
  381. s += '**' + c.attrib['name'] + '**'
  382. if c.text.strip() != '':
  383. s += ' - ' + c.text.strip()
  384. f.write(s + '\n')
  385. f.write('\n')
  386. constants = node.find('constants')
  387. if constants != None and len(list(constants)) > 0:
  388. f.write(make_heading('Numeric Constants', '-'))
  389. for c in list(constants):
  390. s = '- '
  391. s += '**' + c.attrib['name'] + '**'
  392. if 'value' in c.attrib:
  393. s += ' = **' + c.attrib['value'] + '**'
  394. if c.text.strip() != '':
  395. s += ' --- ' + rstize_text(c.text.strip(), name)
  396. f.write(s + '\n')
  397. f.write('\n')
  398. descr = node.find('description')
  399. if descr != None and descr.text.strip() != '':
  400. f.write(make_heading('Description', '-'))
  401. f.write(rstize_text(descr.text.strip(), name) + "\n\n")
  402. methods = node.find('methods')
  403. if methods != None and len(list(methods)) > 0:
  404. f.write(make_heading('Member Function Description', '-'))
  405. for m in list(methods):
  406. f.write(".. _class_" + name + "_" + m.attrib['name'] + ":\n\n")
  407. make_method(f, node.attrib['name'], m, True, name)
  408. f.write('\n')
  409. d = m.find('description')
  410. if d == None or d.text.strip() == '':
  411. continue
  412. f.write(rstize_text(d.text.strip(), name))
  413. f.write("\n\n")
  414. f.write('\n')
  415. file_list = []
  416. for path in input_list:
  417. if os.path.basename(path) == 'modules':
  418. for subdir, dirs, _ in os.walk(path):
  419. if 'doc_classes' in dirs:
  420. doc_dir = os.path.join(subdir, 'doc_classes')
  421. class_file_names = [f for f in os.listdir(doc_dir) if f.endswith('.xml')]
  422. file_list += [os.path.join(doc_dir, f) for f in class_file_names]
  423. elif not os.path.isfile(path):
  424. file_list += [os.path.join(path, f) for f in os.listdir(path) if f.endswith('.xml')]
  425. elif os.path.isfile(path) and path.endswith('.xml'):
  426. file_list.append(path)
  427. for file in file_list:
  428. tree = ET.parse(file)
  429. doc = tree.getroot()
  430. if 'version' not in doc.attrib:
  431. print("Version missing from 'doc'")
  432. sys.exit(255)
  433. version = doc.attrib['version']
  434. if doc.attrib['name'] in class_names:
  435. continue
  436. class_names.append(doc.attrib['name'])
  437. classes[doc.attrib['name']] = doc
  438. class_names.sort()
  439. # Don't make class list for Sphinx, :toctree: handles it
  440. # make_class_list(class_names, 2)
  441. for cn in class_names:
  442. c = classes[cn]
  443. make_rst_class(c)