makerst.py 18 KB

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