makerst.py 17 KB

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