makerst.py 19 KB

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