makerst.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  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. escape_post = False
  153. if tag_text in class_names:
  154. tag_text = make_type(tag_text)
  155. escape_post = True
  156. else: # command
  157. cmd = tag_text
  158. space_pos = tag_text.find(' ')
  159. if cmd == '/codeblock':
  160. tag_text = ''
  161. inside_code = False
  162. # Strip newline if the tag was alone on one
  163. if pre_text[-1] == '\n':
  164. pre_text = pre_text[:-1]
  165. elif cmd == '/code':
  166. tag_text = '``'
  167. inside_code = False
  168. elif inside_code:
  169. tag_text = '[' + tag_text + ']'
  170. elif cmd.find('html') == 0:
  171. cmd = tag_text[:space_pos]
  172. param = tag_text[space_pos + 1:]
  173. tag_text = param
  174. elif cmd.find('method') == 0 or cmd.find('member') == 0 or cmd.find('signal') == 0:
  175. cmd = tag_text[:space_pos]
  176. param = tag_text[space_pos + 1:]
  177. if param.find('.') != -1:
  178. (class_param, method_param) = param.split('.')
  179. tag_text = ':ref:`' + class_param + '.' + method_param + '<class_' + class_param + '_' + method_param + '>`'
  180. else:
  181. tag_text = ':ref:`' + param + '<class_' + cclass + "_" + param + '>`'
  182. escape_post = True
  183. elif cmd.find('image=') == 0:
  184. tag_text = "" # '![](' + cmd[6:] + ')'
  185. elif cmd.find('url=') == 0:
  186. tag_text = ':ref:`' + cmd[4:] + '<' + cmd[4:] + ">`"
  187. elif cmd == '/url':
  188. tag_text = ''
  189. escape_post = True
  190. elif cmd == 'center':
  191. tag_text = ''
  192. elif cmd == '/center':
  193. tag_text = ''
  194. elif cmd == 'codeblock':
  195. tag_text = '\n::\n'
  196. inside_code = True
  197. elif cmd == 'br':
  198. # Make a new paragraph instead of a linebreak, rst is not so linebreak friendly
  199. tag_text = '\n\n'
  200. # Strip potential leading spaces
  201. while post_text[0] == ' ':
  202. post_text = post_text[1:]
  203. elif cmd == 'i' or cmd == '/i':
  204. tag_text = '*'
  205. elif cmd == 'b' or cmd == '/b':
  206. tag_text = '**'
  207. elif cmd == 'u' or cmd == '/u':
  208. tag_text = ''
  209. elif cmd == 'code':
  210. tag_text = '``'
  211. inside_code = True
  212. else:
  213. tag_text = make_type(tag_text)
  214. escape_post = True
  215. # Properly escape things like `[Node]s`
  216. if escape_post and post_text and post_text[0].isalnum(): # not punctuation, escape
  217. post_text = '\ ' + post_text
  218. text = pre_text + tag_text + post_text
  219. pos = len(pre_text) + len(tag_text)
  220. return text
  221. def make_type(t):
  222. global class_names
  223. if t in class_names:
  224. return ':ref:`' + t + '<class_' + t.lower() + '>`'
  225. return t
  226. def make_method(
  227. f,
  228. name,
  229. m,
  230. declare,
  231. cname,
  232. event=False,
  233. pp=None
  234. ):
  235. if (declare or pp == None):
  236. t = '- '
  237. else:
  238. t = ""
  239. ret_type = 'void'
  240. args = list(m)
  241. mdata = {}
  242. mdata['argidx'] = []
  243. for a in args:
  244. if a.tag == 'return':
  245. idx = -1
  246. elif a.tag == 'argument':
  247. idx = int(a.attrib['index'])
  248. else:
  249. continue
  250. mdata['argidx'].append(idx)
  251. mdata[idx] = a
  252. if not event:
  253. if -1 in mdata['argidx']:
  254. t += make_type(mdata[-1].attrib['type'])
  255. else:
  256. t += 'void'
  257. t += ' '
  258. if declare or pp == None:
  259. s = '**' + m.attrib['name'] + '** '
  260. else:
  261. s = ':ref:`' + m.attrib['name'] + '<class_' + cname + "_" + m.attrib['name'] + '>` '
  262. s += '**(**'
  263. argfound = False
  264. for a in mdata['argidx']:
  265. arg = mdata[a]
  266. if a < 0:
  267. continue
  268. if a > 0:
  269. s += ', '
  270. else:
  271. s += ' '
  272. s += make_type(arg.attrib['type'])
  273. if 'name' in arg.attrib:
  274. s += ' ' + arg.attrib['name']
  275. else:
  276. s += ' arg' + str(a)
  277. if 'default' in arg.attrib:
  278. s += '=' + arg.attrib['default']
  279. s += ' **)**'
  280. if 'qualifiers' in m.attrib:
  281. s += ' ' + m.attrib['qualifiers']
  282. if (not declare):
  283. if (pp != None):
  284. pp.append((t, s))
  285. else:
  286. f.write("- " + t + " " + s + "\n")
  287. else:
  288. f.write(t + s + "\n")
  289. def make_heading(title, underline):
  290. return title + '\n' + underline * len(title) + "\n\n"
  291. def make_rst_class(node):
  292. name = node.attrib['name']
  293. f = codecs.open("class_" + name.lower() + '.rst', 'wb', 'utf-8')
  294. # Warn contributors not to edit this file directly
  295. f.write(".. Generated automatically by doc/tools/makerst.py in Godot's source tree.\n")
  296. f.write(".. DO NOT EDIT THIS FILE, but the " + name + ".xml source instead.\n")
  297. f.write(".. The source is found in doc/classes or modules/<name>/doc_classes.\n\n")
  298. f.write(".. _class_" + name + ":\n\n")
  299. f.write(make_heading(name, '='))
  300. if 'inherits' in node.attrib:
  301. inh = node.attrib['inherits'].strip()
  302. f.write('**Inherits:** ')
  303. first = True
  304. while (inh in classes):
  305. if (not first):
  306. f.write(" **<** ")
  307. else:
  308. first = False
  309. f.write(make_type(inh))
  310. inode = classes[inh]
  311. if ('inherits' in inode.attrib):
  312. inh = inode.attrib['inherits'].strip()
  313. else:
  314. inh = None
  315. f.write("\n\n")
  316. inherited = []
  317. for cn in classes:
  318. c = classes[cn]
  319. if 'inherits' in c.attrib:
  320. if (c.attrib['inherits'].strip() == name):
  321. inherited.append(c.attrib['name'])
  322. if (len(inherited)):
  323. f.write('**Inherited By:** ')
  324. for i in range(len(inherited)):
  325. if (i > 0):
  326. f.write(", ")
  327. f.write(make_type(inherited[i]))
  328. f.write("\n\n")
  329. if 'category' in node.attrib:
  330. f.write('**Category:** ' + node.attrib['category'].strip() + "\n\n")
  331. f.write(make_heading('Brief Description', '-'))
  332. briefd = node.find('brief_description')
  333. if briefd != None:
  334. f.write(rstize_text(briefd.text.strip(), name) + "\n\n")
  335. methods = node.find('methods')
  336. if methods != None and len(list(methods)) > 0:
  337. f.write(make_heading('Member Functions', '-'))
  338. ml = []
  339. for m in list(methods):
  340. make_method(f, node.attrib['name'], m, False, name, False, ml)
  341. longest_t = 0
  342. longest_s = 0
  343. for s in ml:
  344. sl = len(s[0])
  345. if (sl > longest_s):
  346. longest_s = sl
  347. tl = len(s[1])
  348. if (tl > longest_t):
  349. longest_t = tl
  350. sep = "+"
  351. for i in range(longest_s + 2):
  352. sep += "-"
  353. sep += "+"
  354. for i in range(longest_t + 2):
  355. sep += "-"
  356. sep += "+\n"
  357. f.write(sep)
  358. for s in ml:
  359. rt = s[0]
  360. while (len(rt) < longest_s):
  361. rt += " "
  362. st = s[1]
  363. while (len(st) < longest_t):
  364. st += " "
  365. f.write("| " + rt + " | " + st + " |\n")
  366. f.write(sep)
  367. f.write('\n')
  368. events = node.find('signals')
  369. if events != None and len(list(events)) > 0:
  370. f.write(make_heading('Signals', '-'))
  371. for m in list(events):
  372. f.write(".. _class_" + name + "_" + m.attrib['name'] + ":\n\n")
  373. make_method(f, node.attrib['name'], m, True, name, True)
  374. f.write('\n')
  375. d = m.find('description')
  376. if d == None or d.text.strip() == '':
  377. continue
  378. f.write(rstize_text(d.text.strip(), name))
  379. f.write("\n\n")
  380. f.write('\n')
  381. members = node.find('members')
  382. if members != None and len(list(members)) > 0:
  383. f.write(make_heading('Member Variables', '-'))
  384. for c in list(members):
  385. # Leading two spaces necessary to prevent breaking the <ul>
  386. f.write(" .. _class_" + name + "_" + c.attrib['name'] + ":\n\n")
  387. s = '- '
  388. s += make_type(c.attrib['type']) + ' '
  389. s += '**' + c.attrib['name'] + '**'
  390. if c.text.strip() != '':
  391. s += ' - ' + rstize_text(c.text.strip(), name)
  392. f.write(s + '\n\n')
  393. f.write('\n')
  394. constants = node.find('constants')
  395. if constants != None and len(list(constants)) > 0:
  396. f.write(make_heading('Numeric Constants', '-'))
  397. for c in list(constants):
  398. s = '- '
  399. s += '**' + c.attrib['name'] + '**'
  400. if 'value' in c.attrib:
  401. s += ' = **' + c.attrib['value'] + '**'
  402. if c.text.strip() != '':
  403. s += ' --- ' + rstize_text(c.text.strip(), name)
  404. f.write(s + '\n')
  405. f.write('\n')
  406. descr = node.find('description')
  407. if descr != None and descr.text.strip() != '':
  408. f.write(make_heading('Description', '-'))
  409. f.write(rstize_text(descr.text.strip(), name) + "\n\n")
  410. methods = node.find('methods')
  411. if methods != None and len(list(methods)) > 0:
  412. f.write(make_heading('Member Function Description', '-'))
  413. for m in list(methods):
  414. f.write(".. _class_" + name + "_" + m.attrib['name'] + ":\n\n")
  415. make_method(f, node.attrib['name'], m, True, name)
  416. f.write('\n')
  417. d = m.find('description')
  418. if d == None or d.text.strip() == '':
  419. continue
  420. f.write(rstize_text(d.text.strip(), name))
  421. f.write("\n\n")
  422. f.write('\n')
  423. file_list = []
  424. for path in input_list:
  425. if os.path.basename(path) == 'modules':
  426. for subdir, dirs, _ in os.walk(path):
  427. if 'doc_classes' in dirs:
  428. doc_dir = os.path.join(subdir, 'doc_classes')
  429. class_file_names = [f for f in os.listdir(doc_dir) if f.endswith('.xml')]
  430. file_list += [os.path.join(doc_dir, f) for f in class_file_names]
  431. elif not os.path.isfile(path):
  432. file_list += [os.path.join(path, f) for f in os.listdir(path) if f.endswith('.xml')]
  433. elif os.path.isfile(path) and path.endswith('.xml'):
  434. file_list.append(path)
  435. for file in file_list:
  436. tree = ET.parse(file)
  437. doc = tree.getroot()
  438. if 'version' not in doc.attrib:
  439. print("Version missing from 'doc'")
  440. sys.exit(255)
  441. version = doc.attrib['version']
  442. if doc.attrib['name'] in class_names:
  443. continue
  444. class_names.append(doc.attrib['name'])
  445. classes[doc.attrib['name']] = doc
  446. class_names.sort()
  447. # Don't make class list for Sphinx, :toctree: handles it
  448. # make_class_list(class_names, 2)
  449. for cn in class_names:
  450. c = classes[cn]
  451. make_rst_class(c)