makerst.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. import codecs
  4. import sys
  5. import os
  6. import re
  7. import xml.etree.ElementTree as ET
  8. input_list = []
  9. cur_file = ""
  10. # http(s)://docs.godotengine.org/<langcode>/<tag>/path/to/page.html(#fragment-tag)
  11. godot_docs_pattern = re.compile('^http(?:s)?:\/\/docs\.godotengine\.org\/(?:[a-zA-Z0-9\.\-_]*)\/(?:[a-zA-Z0-9\.\-_]*)\/(.*)\.html(#.*)?$')
  12. for arg in sys.argv[1:]:
  13. if arg.endswith(os.sep):
  14. arg = arg[:-1]
  15. input_list.append(arg)
  16. if len(input_list) < 1:
  17. print('usage: makerst.py <path to folders> and/or <path to .xml files> (order of arguments irrelevant)')
  18. print('example: makerst.py "../../modules/" "../classes" path_to/some_class.xml')
  19. sys.exit(0)
  20. def validate_tag(elem, tag):
  21. if elem.tag != tag:
  22. print("Tag mismatch, expected '" + tag + "', got " + elem.tag)
  23. sys.exit(255)
  24. class_names = []
  25. classes = {}
  26. def ul_string(str, ul):
  27. str += "\n"
  28. for i in range(len(str) - 1):
  29. str += ul
  30. str += "\n"
  31. return str
  32. def make_class_list(class_list, columns):
  33. f = codecs.open('class_list.rst', 'wb', 'utf-8')
  34. prev = 0
  35. col_max = len(class_list) / columns + 1
  36. print(('col max is ', col_max))
  37. col_count = 0
  38. row_count = 0
  39. last_initial = ''
  40. fit_columns = []
  41. for n in range(0, columns):
  42. fit_columns += [[]]
  43. indexers = []
  44. last_initial = ''
  45. idx = 0
  46. for n in class_list:
  47. col = idx / col_max
  48. if col >= columns:
  49. col = columns - 1
  50. fit_columns[col] += [n]
  51. idx += 1
  52. if n[:1] != last_initial:
  53. indexers += [n]
  54. last_initial = n[:1]
  55. row_max = 0
  56. f.write("\n")
  57. for n in range(0, columns):
  58. if len(fit_columns[n]) > row_max:
  59. row_max = len(fit_columns[n])
  60. f.write("| ")
  61. for n in range(0, columns):
  62. f.write(" | |")
  63. f.write("\n")
  64. f.write("+")
  65. for n in range(0, columns):
  66. f.write("--+-------+")
  67. f.write("\n")
  68. for r in range(0, row_max):
  69. s = '+ '
  70. for c in range(0, columns):
  71. if r >= len(fit_columns[c]):
  72. continue
  73. classname = fit_columns[c][r]
  74. initial = classname[0]
  75. if classname in indexers:
  76. s += '**' + initial + '** | '
  77. else:
  78. s += ' | '
  79. s += '[' + classname + '](class_' + classname.lower() + ') | '
  80. s += '\n'
  81. f.write(s)
  82. for n in range(0, columns):
  83. f.write("--+-------+")
  84. f.write("\n")
  85. f.close()
  86. def rstize_text(text, cclass):
  87. # Linebreak + tabs in the XML should become two line breaks unless in a "codeblock"
  88. pos = 0
  89. while True:
  90. pos = text.find('\n', pos)
  91. if pos == -1:
  92. break
  93. pre_text = text[:pos]
  94. while text[pos + 1] == '\t':
  95. pos += 1
  96. post_text = text[pos + 1:]
  97. # Handle codeblocks
  98. if post_text.startswith("[codeblock]"):
  99. end_pos = post_text.find("[/codeblock]")
  100. if end_pos == -1:
  101. sys.exit("ERROR! [codeblock] without a closing tag!")
  102. code_text = post_text[len("[codeblock]"):end_pos]
  103. post_text = post_text[end_pos:]
  104. # Remove extraneous tabs
  105. code_pos = 0
  106. while True:
  107. code_pos = code_text.find('\n', code_pos)
  108. if code_pos == -1:
  109. break
  110. to_skip = 0
  111. while code_pos + to_skip + 1 < len(code_text) and code_text[code_pos + to_skip + 1] == '\t':
  112. to_skip += 1
  113. if len(code_text[code_pos + to_skip + 1:]) == 0:
  114. code_text = code_text[:code_pos] + "\n"
  115. code_pos += 1
  116. else:
  117. code_text = code_text[:code_pos] + "\n " + code_text[code_pos + to_skip + 1:]
  118. code_pos += 5 - to_skip
  119. text = pre_text + "\n[codeblock]" + code_text + post_text
  120. pos += len("\n[codeblock]" + code_text)
  121. # Handle normal text
  122. else:
  123. text = pre_text + "\n\n" + post_text
  124. pos += 2
  125. next_brac_pos = text.find('[')
  126. # Escape \ character, otherwise it ends up as an escape character in rst
  127. pos = 0
  128. while True:
  129. pos = text.find('\\', pos, next_brac_pos)
  130. if pos == -1:
  131. break
  132. text = text[:pos] + "\\\\" + text[pos + 1:]
  133. pos += 2
  134. # Escape * character to avoid interpreting it as emphasis
  135. pos = 0
  136. while True:
  137. pos = text.find('*', pos, next_brac_pos)
  138. if pos == -1:
  139. break
  140. text = text[:pos] + "\*" + text[pos + 1:]
  141. pos += 2
  142. # Escape _ character at the end of a word to avoid interpreting it as an inline hyperlink
  143. pos = 0
  144. while True:
  145. pos = text.find('_', pos, next_brac_pos)
  146. if pos == -1:
  147. break
  148. if not text[pos + 1].isalnum(): # don't escape within a snake_case word
  149. text = text[:pos] + "\_" + text[pos + 1:]
  150. pos += 2
  151. else:
  152. pos += 1
  153. # Handle [tags]
  154. inside_code = False
  155. pos = 0
  156. while True:
  157. pos = text.find('[', pos)
  158. if pos == -1:
  159. break
  160. endq_pos = text.find(']', pos + 1)
  161. if endq_pos == -1:
  162. break
  163. pre_text = text[:pos]
  164. post_text = text[endq_pos + 1:]
  165. tag_text = text[pos + 1:endq_pos]
  166. escape_post = False
  167. if tag_text in class_names:
  168. tag_text = make_type(tag_text)
  169. escape_post = True
  170. else: # command
  171. cmd = tag_text
  172. space_pos = tag_text.find(' ')
  173. if cmd == '/codeblock':
  174. tag_text = ''
  175. inside_code = False
  176. # Strip newline if the tag was alone on one
  177. if pre_text[-1] == '\n':
  178. pre_text = pre_text[:-1]
  179. elif cmd == '/code':
  180. tag_text = '``'
  181. inside_code = False
  182. escape_post = True
  183. elif inside_code:
  184. tag_text = '[' + tag_text + ']'
  185. elif cmd.find('html') == 0:
  186. cmd = tag_text[:space_pos]
  187. param = tag_text[space_pos + 1:]
  188. tag_text = param
  189. elif cmd.find('method') == 0 or cmd.find('member') == 0 or cmd.find('signal') == 0:
  190. cmd = tag_text[:space_pos]
  191. param = tag_text[space_pos + 1:]
  192. if param.find('.') != -1:
  193. ss = param.split('.')
  194. if len(ss) > 2:
  195. sys.exit("Bad reference: '" + param + "' in file: " + cur_file)
  196. (class_param, method_param) = ss
  197. tag_text = ':ref:`' + class_param + '.' + method_param + '<class_' + class_param + '_' + method_param + '>`'
  198. else:
  199. tag_text = ':ref:`' + param + '<class_' + cclass + "_" + param + '>`'
  200. escape_post = True
  201. elif cmd.find('image=') == 0:
  202. tag_text = "" # '![](' + cmd[6:] + ')'
  203. elif cmd.find('url=') == 0:
  204. tag_text = ':ref:`' + cmd[4:] + '<' + cmd[4:] + ">`"
  205. elif cmd == '/url':
  206. tag_text = ''
  207. escape_post = True
  208. elif cmd == 'center':
  209. tag_text = ''
  210. elif cmd == '/center':
  211. tag_text = ''
  212. elif cmd == 'codeblock':
  213. tag_text = '\n::\n'
  214. inside_code = True
  215. elif cmd == 'br':
  216. # Make a new paragraph instead of a linebreak, rst is not so linebreak friendly
  217. tag_text = '\n\n'
  218. # Strip potential leading spaces
  219. while post_text[0] == ' ':
  220. post_text = post_text[1:]
  221. elif cmd == 'i' or cmd == '/i':
  222. tag_text = '*'
  223. elif cmd == 'b' or cmd == '/b':
  224. tag_text = '**'
  225. elif cmd == 'u' or cmd == '/u':
  226. tag_text = ''
  227. elif cmd == 'code':
  228. tag_text = '``'
  229. inside_code = True
  230. elif cmd.startswith('enum '):
  231. tag_text = make_enum(cmd[5:])
  232. else:
  233. tag_text = make_type(tag_text)
  234. escape_post = True
  235. # Properly escape things like `[Node]s`
  236. if escape_post and post_text and post_text[0].isalnum(): # not punctuation, escape
  237. post_text = '\ ' + post_text
  238. next_brac_pos = post_text.find('[', 0)
  239. iter_pos = 0
  240. while not inside_code:
  241. iter_pos = post_text.find('*', iter_pos, next_brac_pos)
  242. if iter_pos == -1:
  243. break
  244. post_text = post_text[:iter_pos] + "\*" + post_text[iter_pos + 1:]
  245. iter_pos += 2
  246. iter_pos = 0
  247. while not inside_code:
  248. iter_pos = post_text.find('_', iter_pos, next_brac_pos)
  249. if iter_pos == -1:
  250. break
  251. if not post_text[iter_pos + 1].isalnum(): # don't escape within a snake_case word
  252. post_text = post_text[:iter_pos] + "\_" + post_text[iter_pos + 1:]
  253. iter_pos += 2
  254. else:
  255. iter_pos += 1
  256. text = pre_text + tag_text + post_text
  257. pos = len(pre_text) + len(tag_text)
  258. return text
  259. def format_table(f, pp):
  260. longest_t = 0
  261. longest_s = 0
  262. for s in pp:
  263. sl = len(s[0])
  264. if (sl > longest_s):
  265. longest_s = sl
  266. tl = len(s[1])
  267. if (tl > longest_t):
  268. longest_t = tl
  269. sep = "+"
  270. for i in range(longest_s + 2):
  271. sep += "-"
  272. sep += "+"
  273. for i in range(longest_t + 2):
  274. sep += "-"
  275. sep += "+\n"
  276. f.write(sep)
  277. for s in pp:
  278. rt = s[0]
  279. while (len(rt) < longest_s):
  280. rt += " "
  281. st = s[1]
  282. while (len(st) < longest_t):
  283. st += " "
  284. f.write("| " + rt + " | " + st + " |\n")
  285. f.write(sep)
  286. f.write('\n')
  287. def make_type(t):
  288. global class_names
  289. if t in class_names:
  290. return ':ref:`' + t + '<class_' + t + '>`'
  291. return t
  292. def make_enum(t):
  293. global class_names
  294. p = t.find(".")
  295. # Global enums such as Error are relative to @GlobalScope.
  296. if p >= 0:
  297. c = t[0:p]
  298. e = t[p + 1:]
  299. # Variant enums live in GlobalScope but still use periods.
  300. if c == "Variant":
  301. c = "@GlobalScope"
  302. e = "Variant." + e
  303. else:
  304. # Things in GlobalScope don't have a period.
  305. c = "@GlobalScope"
  306. e = t
  307. if c in class_names:
  308. return ':ref:`' + e + '<enum_' + c + '_' + e + '>`'
  309. return t
  310. def make_method(
  311. f,
  312. cname,
  313. method_data,
  314. declare,
  315. event=False,
  316. pp=None
  317. ):
  318. if (declare or pp == None):
  319. t = '- '
  320. else:
  321. t = ""
  322. ret_type = 'void'
  323. args = list(method_data)
  324. mdata = {}
  325. mdata['argidx'] = []
  326. for a in args:
  327. if a.tag == 'return':
  328. idx = -1
  329. elif a.tag == 'argument':
  330. idx = int(a.attrib['index'])
  331. else:
  332. continue
  333. mdata['argidx'].append(idx)
  334. mdata[idx] = a
  335. if not event:
  336. if -1 in mdata['argidx']:
  337. if 'enum' in mdata[-1].attrib:
  338. t += make_enum(mdata[-1].attrib['enum'])
  339. else:
  340. t += make_type(mdata[-1].attrib['type'])
  341. else:
  342. t += 'void'
  343. t += ' '
  344. if declare or pp == None:
  345. s = '**' + method_data.attrib['name'] + '** '
  346. else:
  347. s = ':ref:`' + method_data.attrib['name'] + '<class_' + cname + "_" + method_data.attrib['name'] + '>` '
  348. s += '**(**'
  349. argfound = False
  350. for a in mdata['argidx']:
  351. arg = mdata[a]
  352. if a < 0:
  353. continue
  354. if a > 0:
  355. s += ', '
  356. else:
  357. s += ' '
  358. if 'enum' in arg.attrib:
  359. s += make_enum(arg.attrib['enum'])
  360. else:
  361. s += make_type(arg.attrib['type'])
  362. if 'name' in arg.attrib:
  363. s += ' ' + arg.attrib['name']
  364. else:
  365. s += ' arg' + str(a)
  366. if 'default' in arg.attrib:
  367. s += '=' + arg.attrib['default']
  368. s += ' **)**'
  369. if 'qualifiers' in method_data.attrib:
  370. s += ' ' + method_data.attrib['qualifiers']
  371. if (not declare):
  372. if (pp != None):
  373. pp.append((t, s))
  374. else:
  375. f.write("- " + t + " " + s + "\n")
  376. else:
  377. f.write(t + s + "\n")
  378. def make_properties(
  379. f,
  380. cname,
  381. prop_data,
  382. description=False,
  383. pp=None
  384. ):
  385. t = ""
  386. if 'enum' in prop_data.attrib:
  387. t += make_enum(prop_data.attrib['enum'])
  388. else:
  389. t += make_type(prop_data.attrib['type'])
  390. if description:
  391. s = '**' + prop_data.attrib['name'] + '**'
  392. setget = []
  393. if 'setter' in prop_data.attrib and prop_data.attrib['setter'] != '' and not prop_data.attrib['setter'].startswith('_'):
  394. setget.append(("*Setter*", prop_data.attrib['setter'] + '(value)'))
  395. if 'getter' in prop_data.attrib and prop_data.attrib['getter'] != '' and not prop_data.attrib['getter'].startswith('_'):
  396. setget.append(('*Getter*', prop_data.attrib['getter'] + '()'))
  397. else:
  398. s = ':ref:`' + prop_data.attrib['name'] + '<class_' + cname + "_" + prop_data.attrib['name'] + '>`'
  399. if (pp != None):
  400. pp.append((t, s))
  401. elif description:
  402. f.write('- ' + t + ' ' + s + '\n\n')
  403. if len(setget) > 0:
  404. format_table(f, setget)
  405. def make_heading(title, underline):
  406. return title + '\n' + underline * len(title) + "\n\n"
  407. def make_rst_class(node):
  408. name = node.attrib['name']
  409. f = codecs.open("class_" + name.lower() + '.rst', 'wb', 'utf-8')
  410. # Warn contributors not to edit this file directly
  411. f.write(".. Generated automatically by doc/tools/makerst.py in Godot's source tree.\n")
  412. f.write(".. DO NOT EDIT THIS FILE, but the " + name + ".xml source instead.\n")
  413. f.write(".. The source is found in doc/classes or modules/<name>/doc_classes.\n\n")
  414. f.write(".. _class_" + name + ":\n\n")
  415. f.write(make_heading(name, '='))
  416. # Inheritance tree
  417. # Ascendents
  418. if 'inherits' in node.attrib:
  419. inh = node.attrib['inherits'].strip()
  420. f.write('**Inherits:** ')
  421. first = True
  422. while (inh in classes):
  423. if (not first):
  424. f.write(" **<** ")
  425. else:
  426. first = False
  427. f.write(make_type(inh))
  428. inode = classes[inh]
  429. if ('inherits' in inode.attrib):
  430. inh = inode.attrib['inherits'].strip()
  431. else:
  432. inh = None
  433. f.write("\n\n")
  434. # Descendents
  435. inherited = []
  436. for cn in classes:
  437. c = classes[cn]
  438. if 'inherits' in c.attrib:
  439. if (c.attrib['inherits'].strip() == name):
  440. inherited.append(c.attrib['name'])
  441. if (len(inherited)):
  442. f.write('**Inherited By:** ')
  443. for i in range(len(inherited)):
  444. if (i > 0):
  445. f.write(", ")
  446. f.write(make_type(inherited[i]))
  447. f.write("\n\n")
  448. # Category
  449. if 'category' in node.attrib:
  450. f.write('**Category:** ' + node.attrib['category'].strip() + "\n\n")
  451. # Brief description
  452. f.write(make_heading('Brief Description', '-'))
  453. briefd = node.find('brief_description')
  454. if briefd != None:
  455. f.write(rstize_text(briefd.text.strip(), name) + "\n\n")
  456. # Properties overview
  457. members = node.find('members')
  458. if members != None and len(list(members)) > 0:
  459. f.write(make_heading('Properties', '-'))
  460. ml = []
  461. for m in list(members):
  462. make_properties(f, name, m, False, ml)
  463. format_table(f, ml)
  464. # Methods overview
  465. methods = node.find('methods')
  466. if methods != None and len(list(methods)) > 0:
  467. f.write(make_heading('Methods', '-'))
  468. ml = []
  469. for m in list(methods):
  470. make_method(f, name, m, False, False, ml)
  471. format_table(f, ml)
  472. # Theme properties
  473. theme_items = node.find('theme_items')
  474. if theme_items != None and len(list(theme_items)) > 0:
  475. f.write(make_heading('Theme Properties', '-'))
  476. ml = []
  477. for m in list(theme_items):
  478. make_properties(f, name, m, False, ml)
  479. format_table(f, ml)
  480. # Signals
  481. events = node.find('signals')
  482. if events != None and len(list(events)) > 0:
  483. f.write(make_heading('Signals', '-'))
  484. for m in list(events):
  485. f.write(" .. _class_" + name + "_" + m.attrib['name'] + ":\n\n")
  486. make_method(f, name, m, True, True)
  487. f.write('\n')
  488. d = m.find('description')
  489. if d == None or d.text.strip() == '':
  490. continue
  491. f.write(rstize_text(d.text.strip(), name))
  492. f.write("\n\n")
  493. # Constants and enums
  494. constants = node.find('constants')
  495. consts = []
  496. enum_names = set()
  497. enums = []
  498. if constants != None and len(list(constants)) > 0:
  499. for c in list(constants):
  500. if 'enum' in c.attrib:
  501. enum_names.add(c.attrib['enum'])
  502. enums.append(c)
  503. else:
  504. consts.append(c)
  505. # Enums
  506. if len(enum_names) > 0:
  507. f.write(make_heading('Enumerations', '-'))
  508. for e in enum_names:
  509. f.write(" .. _enum_" + name + "_" + e + ":\n\n")
  510. f.write("enum **" + e + "**:\n\n")
  511. for c in enums:
  512. if c.attrib['enum'] != e:
  513. continue
  514. s = '- '
  515. s += '**' + c.attrib['name'] + '**'
  516. if 'value' in c.attrib:
  517. s += ' = **' + c.attrib['value'] + '**'
  518. if c.text.strip() != '':
  519. s += ' --- ' + rstize_text(c.text.strip(), name)
  520. f.write(s + '\n')
  521. f.write('\n')
  522. # Constants
  523. if len(consts) > 0:
  524. f.write(make_heading('Constants', '-'))
  525. for c in list(consts):
  526. s = '- '
  527. s += '**' + c.attrib['name'] + '**'
  528. if 'value' in c.attrib:
  529. s += ' = **' + c.attrib['value'] + '**'
  530. if c.text.strip() != '':
  531. s += ' --- ' + rstize_text(c.text.strip(), name)
  532. f.write(s + '\n')
  533. # Class description
  534. descr = node.find('description')
  535. if descr != None and descr.text.strip() != '':
  536. f.write(make_heading('Description', '-'))
  537. f.write(rstize_text(descr.text.strip(), name) + "\n\n")
  538. # Online tutorials
  539. global godot_docs_pattern
  540. tutorials = node.find('tutorials')
  541. if tutorials != None and len(tutorials) > 0:
  542. f.write(make_heading('Tutorials', '-'))
  543. for t in tutorials:
  544. link = t.text.strip()
  545. match = godot_docs_pattern.search(link);
  546. if match:
  547. groups = match.groups()
  548. if match.lastindex == 2:
  549. # Doc reference with fragment identifier: emit direct link to section with reference to page, for example:
  550. # `#calling-javascript-from-script in Exporting For Web`
  551. f.write("- `" + groups[1] + " <../" + groups[0] + ".html" + groups[1] + ">`_ in :doc:`../" + groups[0] + "`\n")
  552. # Commented out alternative: Instead just emit:
  553. # `Subsection in Exporting For Web`
  554. # f.write("- `Subsection <../" + groups[0] + ".html" + groups[1] + ">`_ in :doc:`../" + groups[0] + "`\n")
  555. elif match.lastindex == 1:
  556. # Doc reference, for example:
  557. # `Math`
  558. f.write("- :doc:`../" + groups[0] + "`\n")
  559. else:
  560. # External link, for example:
  561. # `http://enet.bespin.org/usergroup0.html`
  562. f.write("- `" + link + " <" + link + ">`_\n")
  563. # Property descriptions
  564. members = node.find('members')
  565. if members != None and len(list(members)) > 0:
  566. f.write(make_heading('Property Descriptions', '-'))
  567. for m in list(members):
  568. f.write(" .. _class_" + name + "_" + m.attrib['name'] + ":\n\n")
  569. make_properties(f, name, m, True)
  570. if m.text.strip() != '':
  571. f.write(rstize_text(m.text.strip(), name))
  572. f.write('\n\n')
  573. # Method descriptions
  574. methods = node.find('methods')
  575. if methods != None and len(list(methods)) > 0:
  576. f.write(make_heading('Method Descriptions', '-'))
  577. for m in list(methods):
  578. f.write(".. _class_" + name + "_" + m.attrib['name'] + ":\n\n")
  579. make_method(f, name, m, True)
  580. f.write('\n')
  581. d = m.find('description')
  582. if d == None or d.text.strip() == '':
  583. continue
  584. f.write(rstize_text(d.text.strip(), name))
  585. f.write("\n\n")
  586. file_list = []
  587. for path in input_list:
  588. if os.path.basename(path) == 'modules':
  589. for subdir, dirs, _ in os.walk(path):
  590. if 'doc_classes' in dirs:
  591. doc_dir = os.path.join(subdir, 'doc_classes')
  592. class_file_names = [f for f in os.listdir(doc_dir) if f.endswith('.xml')]
  593. file_list += [os.path.join(doc_dir, f) for f in class_file_names]
  594. elif not os.path.isfile(path):
  595. file_list += [os.path.join(path, f) for f in os.listdir(path) if f.endswith('.xml')]
  596. elif os.path.isfile(path) and path.endswith('.xml'):
  597. file_list.append(path)
  598. for cur_file in file_list:
  599. tree = ET.parse(cur_file)
  600. doc = tree.getroot()
  601. if 'version' not in doc.attrib:
  602. print("Version missing from 'doc'")
  603. sys.exit(255)
  604. version = doc.attrib['version']
  605. if doc.attrib['name'] in class_names:
  606. continue
  607. class_names.append(doc.attrib['name'])
  608. classes[doc.attrib['name']] = doc
  609. class_names.sort()
  610. # Don't make class list for Sphinx, :toctree: handles it
  611. # make_class_list(class_names, 2)
  612. for cn in class_names:
  613. c = classes[cn]
  614. make_rst_class(c)