makerst.py 23 KB

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