makerst.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633
  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. elif cmd.startswith('enum '):
  220. tag_text = make_enum(cmd[5:])
  221. else:
  222. tag_text = make_type(tag_text)
  223. escape_post = True
  224. # Properly escape things like `[Node]s`
  225. if escape_post and post_text and post_text[0].isalnum(): # not punctuation, escape
  226. post_text = '\ ' + post_text
  227. next_brac_pos = post_text.find('[', 0)
  228. iter_pos = 0
  229. while not inside_code:
  230. iter_pos = post_text.find('*', iter_pos, next_brac_pos)
  231. if iter_pos == -1:
  232. break
  233. post_text = post_text[:iter_pos] + "\*" + post_text[iter_pos + 1:]
  234. iter_pos += 2
  235. iter_pos = 0
  236. while not inside_code:
  237. iter_pos = post_text.find('_', iter_pos, next_brac_pos)
  238. if iter_pos == -1:
  239. break
  240. if not post_text[iter_pos + 1].isalnum(): # don't escape within a snake_case word
  241. post_text = post_text[:iter_pos] + "\_" + post_text[iter_pos + 1:]
  242. iter_pos += 2
  243. else:
  244. iter_pos += 1
  245. text = pre_text + tag_text + post_text
  246. pos = len(pre_text) + len(tag_text)
  247. return text
  248. def make_type(t):
  249. global class_names
  250. if t in class_names:
  251. return ':ref:`' + t + '<class_' + t.lower() + '>`'
  252. return t
  253. def make_enum(t):
  254. global class_names
  255. p = t.find(".")
  256. # Global enums such as Error are relative to @GlobalScope.
  257. if p >= 0:
  258. c = t[0:p]
  259. e = t[p + 1:]
  260. # Variant enums live in GlobalScope but still use periods.
  261. if c == "Variant":
  262. c = "@GlobalScope"
  263. e = "Variant." + e
  264. else:
  265. # Things in GlobalScope don't have a period.
  266. c = "@GlobalScope"
  267. e = t
  268. if c in class_names:
  269. return ':ref:`' + e + '<enum_' + c.lower() + '_' + e.lower() + '>`'
  270. return t
  271. def make_method(
  272. f,
  273. name,
  274. m,
  275. declare,
  276. cname,
  277. event=False,
  278. pp=None
  279. ):
  280. if (declare or pp == None):
  281. t = '- '
  282. else:
  283. t = ""
  284. ret_type = 'void'
  285. args = list(m)
  286. mdata = {}
  287. mdata['argidx'] = []
  288. for a in args:
  289. if a.tag == 'return':
  290. idx = -1
  291. elif a.tag == 'argument':
  292. idx = int(a.attrib['index'])
  293. else:
  294. continue
  295. mdata['argidx'].append(idx)
  296. mdata[idx] = a
  297. if not event:
  298. if -1 in mdata['argidx']:
  299. if 'enum' in mdata[-1].attrib:
  300. t += make_enum(mdata[-1].attrib['enum'])
  301. else:
  302. t += make_type(mdata[-1].attrib['type'])
  303. else:
  304. t += 'void'
  305. t += ' '
  306. if declare or pp == None:
  307. s = '**' + m.attrib['name'] + '** '
  308. else:
  309. s = ':ref:`' + m.attrib['name'] + '<class_' + cname + "_" + m.attrib['name'] + '>` '
  310. s += '**(**'
  311. argfound = False
  312. for a in mdata['argidx']:
  313. arg = mdata[a]
  314. if a < 0:
  315. continue
  316. if a > 0:
  317. s += ', '
  318. else:
  319. s += ' '
  320. if 'enum' in arg.attrib:
  321. s += make_enum(arg.attrib['enum'])
  322. else:
  323. s += make_type(arg.attrib['type'])
  324. if 'name' in arg.attrib:
  325. s += ' ' + arg.attrib['name']
  326. else:
  327. s += ' arg' + str(a)
  328. if 'default' in arg.attrib:
  329. s += '=' + arg.attrib['default']
  330. s += ' **)**'
  331. if 'qualifiers' in m.attrib:
  332. s += ' ' + m.attrib['qualifiers']
  333. if (not declare):
  334. if (pp != None):
  335. pp.append((t, s))
  336. else:
  337. f.write("- " + t + " " + s + "\n")
  338. else:
  339. f.write(t + s + "\n")
  340. def make_heading(title, underline):
  341. return title + '\n' + underline * len(title) + "\n\n"
  342. def make_rst_class(node):
  343. name = node.attrib['name']
  344. f = codecs.open("class_" + name.lower() + '.rst', 'wb', 'utf-8')
  345. # Warn contributors not to edit this file directly
  346. f.write(".. Generated automatically by doc/tools/makerst.py in Godot's source tree.\n")
  347. f.write(".. DO NOT EDIT THIS FILE, but the " + name + ".xml source instead.\n")
  348. f.write(".. The source is found in doc/classes or modules/<name>/doc_classes.\n\n")
  349. f.write(".. _class_" + name + ":\n\n")
  350. f.write(make_heading(name, '='))
  351. if 'inherits' in node.attrib:
  352. inh = node.attrib['inherits'].strip()
  353. f.write('**Inherits:** ')
  354. first = True
  355. while (inh in classes):
  356. if (not first):
  357. f.write(" **<** ")
  358. else:
  359. first = False
  360. f.write(make_type(inh))
  361. inode = classes[inh]
  362. if ('inherits' in inode.attrib):
  363. inh = inode.attrib['inherits'].strip()
  364. else:
  365. inh = None
  366. f.write("\n\n")
  367. inherited = []
  368. for cn in classes:
  369. c = classes[cn]
  370. if 'inherits' in c.attrib:
  371. if (c.attrib['inherits'].strip() == name):
  372. inherited.append(c.attrib['name'])
  373. if (len(inherited)):
  374. f.write('**Inherited By:** ')
  375. for i in range(len(inherited)):
  376. if (i > 0):
  377. f.write(", ")
  378. f.write(make_type(inherited[i]))
  379. f.write("\n\n")
  380. if 'category' in node.attrib:
  381. f.write('**Category:** ' + node.attrib['category'].strip() + "\n\n")
  382. f.write(make_heading('Brief Description', '-'))
  383. briefd = node.find('brief_description')
  384. if briefd != None:
  385. f.write(rstize_text(briefd.text.strip(), name) + "\n\n")
  386. methods = node.find('methods')
  387. if methods != None and len(list(methods)) > 0:
  388. f.write(make_heading('Member Functions', '-'))
  389. ml = []
  390. for m in list(methods):
  391. make_method(f, node.attrib['name'], m, False, name, False, ml)
  392. longest_t = 0
  393. longest_s = 0
  394. for s in ml:
  395. sl = len(s[0])
  396. if (sl > longest_s):
  397. longest_s = sl
  398. tl = len(s[1])
  399. if (tl > longest_t):
  400. longest_t = tl
  401. sep = "+"
  402. for i in range(longest_s + 2):
  403. sep += "-"
  404. sep += "+"
  405. for i in range(longest_t + 2):
  406. sep += "-"
  407. sep += "+\n"
  408. f.write(sep)
  409. for s in ml:
  410. rt = s[0]
  411. while (len(rt) < longest_s):
  412. rt += " "
  413. st = s[1]
  414. while (len(st) < longest_t):
  415. st += " "
  416. f.write("| " + rt + " | " + st + " |\n")
  417. f.write(sep)
  418. f.write('\n')
  419. events = node.find('signals')
  420. if events != None and len(list(events)) > 0:
  421. f.write(make_heading('Signals', '-'))
  422. for m in list(events):
  423. f.write(".. _class_" + name + "_" + m.attrib['name'] + ":\n\n")
  424. make_method(f, node.attrib['name'], m, True, name, True)
  425. f.write('\n')
  426. d = m.find('description')
  427. if d == None or d.text.strip() == '':
  428. continue
  429. f.write(rstize_text(d.text.strip(), name))
  430. f.write("\n\n")
  431. f.write('\n')
  432. members = node.find('members')
  433. if members != None and len(list(members)) > 0:
  434. f.write(make_heading('Member Variables', '-'))
  435. for c in list(members):
  436. # Leading two spaces necessary to prevent breaking the <ul>
  437. f.write(" .. _class_" + name + "_" + c.attrib['name'] + ":\n\n")
  438. s = '- '
  439. if 'enum' in c.attrib:
  440. s += make_enum(c.attrib['enum']) + ' '
  441. else:
  442. s += make_type(c.attrib['type']) + ' '
  443. s += '**' + c.attrib['name'] + '**'
  444. if c.text.strip() != '':
  445. s += ' - ' + rstize_text(c.text.strip(), name)
  446. f.write(s + '\n\n')
  447. f.write('\n')
  448. constants = node.find('constants')
  449. consts = []
  450. enum_names = set()
  451. enums = []
  452. if constants != None and len(list(constants)) > 0:
  453. for c in list(constants):
  454. if 'enum' in c.attrib:
  455. enum_names.add(c.attrib['enum'])
  456. enums.append(c)
  457. else:
  458. consts.append(c)
  459. if len(consts) > 0:
  460. f.write(make_heading('Numeric Constants', '-'))
  461. for c in list(consts):
  462. s = '- '
  463. s += '**' + c.attrib['name'] + '**'
  464. if 'value' in c.attrib:
  465. s += ' = **' + c.attrib['value'] + '**'
  466. if c.text.strip() != '':
  467. s += ' --- ' + rstize_text(c.text.strip(), name)
  468. f.write(s + '\n')
  469. f.write('\n')
  470. if len(enum_names) > 0:
  471. f.write(make_heading('Enums', '-'))
  472. for e in enum_names:
  473. f.write(" .. _enum_" + name + "_" + e + ":\n\n")
  474. f.write("enum **" + e + "**\n\n")
  475. for c in enums:
  476. if c.attrib['enum'] != e:
  477. continue
  478. s = '- '
  479. s += '**' + c.attrib['name'] + '**'
  480. if 'value' in c.attrib:
  481. s += ' = **' + c.attrib['value'] + '**'
  482. if c.text.strip() != '':
  483. s += ' --- ' + rstize_text(c.text.strip(), name)
  484. f.write(s + '\n')
  485. f.write('\n')
  486. f.write('\n')
  487. descr = node.find('description')
  488. if descr != None and descr.text.strip() != '':
  489. f.write(make_heading('Description', '-'))
  490. f.write(rstize_text(descr.text.strip(), name) + "\n\n")
  491. methods = node.find('methods')
  492. if methods != None and len(list(methods)) > 0:
  493. f.write(make_heading('Member Function Description', '-'))
  494. for m in list(methods):
  495. f.write(".. _class_" + name + "_" + m.attrib['name'] + ":\n\n")
  496. make_method(f, node.attrib['name'], m, True, name)
  497. f.write('\n')
  498. d = m.find('description')
  499. if d == None or d.text.strip() == '':
  500. continue
  501. f.write(rstize_text(d.text.strip(), name))
  502. f.write("\n\n")
  503. f.write('\n')
  504. f.close()
  505. file_list = []
  506. for path in input_list:
  507. if os.path.basename(path) == 'modules':
  508. for subdir, dirs, _ in os.walk(path):
  509. if 'doc_classes' in dirs:
  510. doc_dir = os.path.join(subdir, 'doc_classes')
  511. class_file_names = [f for f in os.listdir(doc_dir) if f.endswith('.xml')]
  512. file_list += [os.path.join(doc_dir, f) for f in class_file_names]
  513. elif not os.path.isfile(path):
  514. file_list += [os.path.join(path, f) for f in os.listdir(path) if f.endswith('.xml')]
  515. elif os.path.isfile(path) and path.endswith('.xml'):
  516. file_list.append(path)
  517. for cur_file in file_list:
  518. tree = ET.parse(cur_file)
  519. doc = tree.getroot()
  520. if 'version' not in doc.attrib:
  521. print("Version missing from 'doc'")
  522. sys.exit(255)
  523. version = doc.attrib['version']
  524. if doc.attrib['name'] in class_names:
  525. continue
  526. class_names.append(doc.attrib['name'])
  527. classes[doc.attrib['name']] = doc
  528. class_names.sort()
  529. # Don't make class list for Sphinx, :toctree: handles it
  530. # make_class_list(class_names, 2)
  531. for cn in class_names:
  532. c = classes[cn]
  533. make_rst_class(c)