makerst.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990
  1. #!/usr/bin/env python3
  2. import argparse
  3. import sys
  4. import os
  5. import re
  6. import xml.etree.ElementTree as ET
  7. from collections import defaultdict, OrderedDict
  8. # Uncomment to do type checks. I have it commented out so it works below Python 3.5
  9. #from typing import List, Dict, TextIO, Tuple, Iterable, Optional, DefaultDict, Any, Union
  10. # http(s)://docs.godotengine.org/<langcode>/<tag>/path/to/page.html(#fragment-tag)
  11. GODOT_DOCS_PATTERN = re.compile(r'^http(?:s)?://docs\.godotengine\.org/(?:[a-zA-Z0-9.\-_]*)/(?:[a-zA-Z0-9.\-_]*)/(.*)\.html(#.*)?$')
  12. def print_error(error, state): # type: (str, State) -> None
  13. print(error)
  14. state.errored = True
  15. class TypeName:
  16. def __init__(self, type_name, enum=None): # type: (str, Optional[str]) -> None
  17. self.type_name = type_name
  18. self.enum = enum
  19. def to_rst(self, state): # type: ("State") -> str
  20. if self.enum is not None:
  21. return make_enum(self.enum, state)
  22. elif self.type_name == "void":
  23. return "void"
  24. else:
  25. return make_type(self.type_name, state)
  26. @classmethod
  27. def from_element(cls, element): # type: (ET.Element) -> "TypeName"
  28. return cls(element.attrib["type"], element.get("enum"))
  29. class PropertyDef:
  30. def __init__(self, name, type_name, setter, getter, text): # type: (str, TypeName, Optional[str], Optional[str], Optional[str]) -> None
  31. self.name = name
  32. self.type_name = type_name
  33. self.setter = setter
  34. self.getter = getter
  35. self.text = text
  36. class ParameterDef:
  37. def __init__(self, name, type_name, default_value): # type: (str, TypeName, Optional[str]) -> None
  38. self.name = name
  39. self.type_name = type_name
  40. self.default_value = default_value
  41. class SignalDef:
  42. def __init__(self, name, parameters, description): # type: (str, List[ParameterDef], Optional[str]) -> None
  43. self.name = name
  44. self.parameters = parameters
  45. self.description = description
  46. class MethodDef:
  47. def __init__(self, name, return_type, parameters, description, qualifiers): # type: (str, TypeName, List[ParameterDef], Optional[str], Optional[str]) -> None
  48. self.name = name
  49. self.return_type = return_type
  50. self.parameters = parameters
  51. self.description = description
  52. self.qualifiers = qualifiers
  53. class ConstantDef:
  54. def __init__(self, name, value, text): # type: (str, str, Optional[str]) -> None
  55. self.name = name
  56. self.value = value
  57. self.text = text
  58. class EnumDef:
  59. def __init__(self, name): # type: (str) -> None
  60. self.name = name
  61. self.values = OrderedDict() # type: OrderedDict[str, ConstantDef]
  62. class ThemeItemDef:
  63. def __init__(self, name, type_name): # type: (str, TypeName) -> None
  64. self.name = name
  65. self.type_name = type_name
  66. class ClassDef:
  67. def __init__(self, name): # type: (str) -> None
  68. self.name = name
  69. self.constants = OrderedDict() # type: OrderedDict[str, ConstantDef]
  70. self.enums = OrderedDict() # type: OrderedDict[str, EnumDef]
  71. self.properties = OrderedDict() # type: OrderedDict[str, PropertyDef]
  72. self.methods = OrderedDict() # type: OrderedDict[str, List[MethodDef]]
  73. self.signals = OrderedDict() # type: OrderedDict[str, SignalDef]
  74. self.inherits = None # type: Optional[str]
  75. self.category = None # type: Optional[str]
  76. self.brief_description = None # type: Optional[str]
  77. self.description = None # type: Optional[str]
  78. self.theme_items = None # type: Optional[OrderedDict[str, List[ThemeItemDef]]]
  79. self.tutorials = [] # type: List[str]
  80. class State:
  81. def __init__(self): # type: () -> None
  82. # Has any error been reported?
  83. self.errored = False
  84. self.classes = OrderedDict() # type: OrderedDict[str, ClassDef]
  85. self.current_class = "" # type: str
  86. def parse_class(self, class_root): # type: (ET.Element) -> None
  87. class_name = class_root.attrib["name"]
  88. class_def = ClassDef(class_name)
  89. self.classes[class_name] = class_def
  90. inherits = class_root.get("inherits")
  91. if inherits is not None:
  92. class_def.inherits = inherits
  93. category = class_root.get("category")
  94. if category is not None:
  95. class_def.category = category
  96. brief_desc = class_root.find("brief_description")
  97. if brief_desc is not None and brief_desc.text:
  98. class_def.brief_description = brief_desc.text
  99. desc = class_root.find("description")
  100. if desc is not None and desc.text:
  101. class_def.description = desc.text
  102. properties = class_root.find("members")
  103. if properties is not None:
  104. for property in properties:
  105. assert property.tag == "member"
  106. property_name = property.attrib["name"]
  107. if property_name in class_def.properties:
  108. print_error("Duplicate property '{}', file: {}".format(property_name, class_name), self)
  109. continue
  110. type_name = TypeName.from_element(property)
  111. setter = property.get("setter") or None # Use or None so '' gets turned into None.
  112. getter = property.get("getter") or None
  113. property_def = PropertyDef(property_name, type_name, setter, getter, property.text)
  114. class_def.properties[property_name] = property_def
  115. methods = class_root.find("methods")
  116. if methods is not None:
  117. for method in methods:
  118. assert method.tag == "method"
  119. method_name = method.attrib["name"]
  120. qualifiers = method.get("qualifiers")
  121. return_element = method.find("return")
  122. if return_element is not None:
  123. return_type = TypeName.from_element(return_element)
  124. else:
  125. return_type = TypeName("void")
  126. params = parse_arguments(method)
  127. desc_element = method.find("description")
  128. method_desc = None
  129. if desc_element is not None:
  130. method_desc = desc_element.text
  131. method_def = MethodDef(method_name, return_type, params, method_desc, qualifiers)
  132. if method_name not in class_def.methods:
  133. class_def.methods[method_name] = []
  134. class_def.methods[method_name].append(method_def)
  135. constants = class_root.find("constants")
  136. if constants is not None:
  137. for constant in constants:
  138. assert constant.tag == "constant"
  139. constant_name = constant.attrib["name"]
  140. value = constant.attrib["value"]
  141. enum = constant.get("enum")
  142. constant_def = ConstantDef(constant_name, value, constant.text)
  143. if enum is None:
  144. if constant_name in class_def.constants:
  145. print_error("Duplicate constant '{}', file: {}".format(constant_name, class_name), self)
  146. continue
  147. class_def.constants[constant_name] = constant_def
  148. else:
  149. if enum in class_def.enums:
  150. enum_def = class_def.enums[enum]
  151. else:
  152. enum_def = EnumDef(enum)
  153. class_def.enums[enum] = enum_def
  154. enum_def.values[constant_name] = constant_def
  155. signals = class_root.find("signals")
  156. if signals is not None:
  157. for signal in signals:
  158. assert signal.tag == "signal"
  159. signal_name = signal.attrib["name"]
  160. if signal_name in class_def.signals:
  161. print_error("Duplicate signal '{}', file: {}".format(signal_name, class_name), self)
  162. continue
  163. params = parse_arguments(signal)
  164. desc_element = signal.find("description")
  165. signal_desc = None
  166. if desc_element is not None:
  167. signal_desc = desc_element.text
  168. signal_def = SignalDef(signal_name, params, signal_desc)
  169. class_def.signals[signal_name] = signal_def
  170. theme_items = class_root.find("theme_items")
  171. if theme_items is not None:
  172. class_def.theme_items = OrderedDict()
  173. for theme_item in theme_items:
  174. assert theme_item.tag == "theme_item"
  175. theme_item_name = theme_item.attrib["name"]
  176. theme_item_def = ThemeItemDef(theme_item_name, TypeName.from_element(theme_item))
  177. if theme_item_name not in class_def.theme_items:
  178. class_def.theme_items[theme_item_name] = []
  179. class_def.theme_items[theme_item_name].append(theme_item_def)
  180. tutorials = class_root.find("tutorials")
  181. if tutorials is not None:
  182. for link in tutorials:
  183. assert link.tag == "link"
  184. if link.text is not None:
  185. class_def.tutorials.append(link.text)
  186. def sort_classes(self): # type: () -> None
  187. self.classes = OrderedDict(sorted(self.classes.items(), key=lambda t: t[0]))
  188. def parse_arguments(root): # type: (ET.Element) -> List[ParameterDef]
  189. param_elements = root.findall("argument")
  190. params = [None] * len(param_elements) # type: Any
  191. for param_element in param_elements:
  192. param_name = param_element.attrib["name"]
  193. index = int(param_element.attrib["index"])
  194. type_name = TypeName.from_element(param_element)
  195. default = param_element.get("default")
  196. params[index] = ParameterDef(param_name, type_name, default)
  197. cast = params # type: List[ParameterDef]
  198. return cast
  199. def main(): # type: () -> None
  200. parser = argparse.ArgumentParser()
  201. parser.add_argument("path", nargs="+", help="A path to an XML file or a directory containing XML files to parse.")
  202. group = parser.add_mutually_exclusive_group()
  203. group.add_argument("--output", "-o", default=".", help="The directory to save output .rst files in.")
  204. group.add_argument("--dry-run", action="store_true", help="If passed, no output will be generated and XML files are only checked for errors.")
  205. args = parser.parse_args()
  206. file_list = [] # type: List[str]
  207. for path in args.path:
  208. # Cut off trailing slashes so os.path.basename doesn't choke.
  209. if path.endswith(os.sep):
  210. path = path[:-1]
  211. if os.path.basename(path) == 'modules':
  212. for subdir, dirs, _ in os.walk(path):
  213. if 'doc_classes' in dirs:
  214. doc_dir = os.path.join(subdir, 'doc_classes')
  215. class_file_names = (f for f in os.listdir(doc_dir) if f.endswith('.xml'))
  216. file_list += (os.path.join(doc_dir, f) for f in class_file_names)
  217. elif os.path.isdir(path):
  218. file_list += (os.path.join(path, f) for f in os.listdir(path) if f.endswith('.xml'))
  219. elif os.path.isfile(path):
  220. if not path.endswith(".xml"):
  221. print("Got non-.xml file '{}' in input, skipping.".format(path))
  222. continue
  223. file_list.append(path)
  224. classes = {} # type: Dict[str, ET.Element]
  225. state = State()
  226. for cur_file in file_list:
  227. try:
  228. tree = ET.parse(cur_file)
  229. except ET.ParseError as e:
  230. print_error("Parse error reading file '{}': {}".format(cur_file, e), state)
  231. continue
  232. doc = tree.getroot()
  233. if 'version' not in doc.attrib:
  234. print_error("Version missing from 'doc', file: {}".format(cur_file), state)
  235. continue
  236. name = doc.attrib["name"]
  237. if name in classes:
  238. print_error("Duplicate class '{}'".format(name), state)
  239. continue
  240. classes[name] = doc
  241. for name, data in classes.items():
  242. try:
  243. state.parse_class(data)
  244. except Exception as e:
  245. print_error("Exception while parsing class '{}': {}".format(name, e), state)
  246. state.sort_classes()
  247. for class_name, class_def in state.classes.items():
  248. state.current_class = class_name
  249. make_rst_class(class_def, state, args.dry_run, args.output)
  250. if state.errored:
  251. exit(1)
  252. def make_rst_class(class_def, state, dry_run, output_dir): # type: (ClassDef, State, bool, str) -> None
  253. class_name = class_def.name
  254. if dry_run:
  255. f = open(os.devnull, "w")
  256. else:
  257. f = open(os.path.join(output_dir, "class_" + class_name.lower() + '.rst'), 'w', encoding='utf-8')
  258. # Warn contributors not to edit this file directly
  259. f.write(".. Generated automatically by doc/tools/makerst.py in Godot's source tree.\n")
  260. f.write(".. DO NOT EDIT THIS FILE, but the " + class_name + ".xml source instead.\n")
  261. f.write(".. The source is found in doc/classes or modules/<name>/doc_classes.\n\n")
  262. f.write(".. _class_" + class_name + ":\n\n")
  263. f.write(make_heading(class_name, '='))
  264. # Inheritance tree
  265. # Ascendants
  266. if class_def.inherits:
  267. inh = class_def.inherits.strip()
  268. f.write('**Inherits:** ')
  269. first = True
  270. while inh in state.classes:
  271. if not first:
  272. f.write(" **<** ")
  273. else:
  274. first = False
  275. f.write(make_type(inh, state))
  276. inode = state.classes[inh].inherits
  277. if inode:
  278. inh = inode.strip()
  279. else:
  280. break
  281. f.write("\n\n")
  282. # Descendents
  283. inherited = []
  284. for c in state.classes.values():
  285. if c.inherits and c.inherits.strip() == class_name:
  286. inherited.append(c.name)
  287. if len(inherited):
  288. f.write('**Inherited By:** ')
  289. for i, child in enumerate(inherited):
  290. if i > 0:
  291. f.write(", ")
  292. f.write(make_type(child, state))
  293. f.write("\n\n")
  294. # Category
  295. if class_def.category is not None:
  296. f.write('**Category:** ' + class_def.category.strip() + "\n\n")
  297. # Brief description
  298. f.write(make_heading('Brief Description', '-'))
  299. if class_def.brief_description is not None:
  300. f.write(rstize_text(class_def.brief_description.strip(), state) + "\n\n")
  301. # Properties overview
  302. if len(class_def.properties) > 0:
  303. f.write(make_heading('Properties', '-'))
  304. ml = [] # type: List[Tuple[str, str]]
  305. for property_def in class_def.properties.values():
  306. type_rst = property_def.type_name.to_rst(state)
  307. ref = ":ref:`{0}<class_{1}_property_{0}>`".format(property_def.name, class_name)
  308. ml.append((type_rst, ref))
  309. format_table(f, ml)
  310. # Methods overview
  311. if len(class_def.methods) > 0:
  312. f.write(make_heading('Methods', '-'))
  313. ml = []
  314. for method_list in class_def.methods.values():
  315. for m in method_list:
  316. ml.append(make_method_signature(class_def, m, True, state))
  317. format_table(f, ml)
  318. # Theme properties
  319. if class_def.theme_items is not None and len(class_def.theme_items) > 0:
  320. f.write(make_heading('Theme Properties', '-'))
  321. ml = []
  322. for theme_item_list in class_def.theme_items.values():
  323. for theme_item in theme_item_list:
  324. ml.append((theme_item.type_name.to_rst(state), theme_item.name))
  325. format_table(f, ml)
  326. # Signals
  327. if len(class_def.signals) > 0:
  328. f.write(make_heading('Signals', '-'))
  329. for signal in class_def.signals.values():
  330. #f.write(".. _class_{}_{}:\n\n".format(class_name, signal.name))
  331. f.write(".. _class_{}_signal_{}:\n\n".format(class_name, signal.name))
  332. _, signature = make_method_signature(class_def, signal, False, state)
  333. f.write("- {}\n\n".format(signature))
  334. if signal.description is None or signal.description.strip() == '':
  335. continue
  336. f.write(rstize_text(signal.description.strip(), state))
  337. f.write("\n\n")
  338. # Enums
  339. if len(class_def.enums) > 0:
  340. f.write(make_heading('Enumerations', '-'))
  341. for e in class_def.enums.values():
  342. f.write(".. _enum_{}_{}:\n\n".format(class_name, e.name))
  343. # Sphinx seems to divide the bullet list into individual <ul> tags if we weave the labels into it.
  344. # As such I'll put them all above the list. Won't be perfect but better than making the list visually broken.
  345. # As to why I'm not modifying the reference parser to directly link to the _enum label:
  346. # If somebody gets annoyed enough to fix it, all existing references will magically improve.
  347. for value in e.values.values():
  348. f.write(".. _class_{}_constant_{}:\n\n".format(class_name, value.name))
  349. f.write("enum **{}**:\n\n".format(e.name))
  350. for value in e.values.values():
  351. f.write("- **{}** = **{}**".format(value.name, value.value))
  352. if value.text is not None and value.text.strip() != '':
  353. f.write(' --- ' + rstize_text(value.text.strip(), state))
  354. f.write('\n\n')
  355. # Constants
  356. if len(class_def.constants) > 0:
  357. f.write(make_heading('Constants', '-'))
  358. # Sphinx seems to divide the bullet list into individual <ul> tags if we weave the labels into it.
  359. # As such I'll put them all above the list. Won't be perfect but better than making the list visually broken.
  360. for constant in class_def.constants.values():
  361. f.write(".. _class_{}_constant_{}:\n\n".format(class_name, constant.name))
  362. for constant in class_def.constants.values():
  363. f.write("- **{}** = **{}**".format(constant.name, constant.value))
  364. if constant.text is not None and constant.text.strip() != '':
  365. f.write(' --- ' + rstize_text(constant.text.strip(), state))
  366. f.write('\n\n')
  367. # Class description
  368. if class_def.description is not None and class_def.description.strip() != '':
  369. f.write(make_heading('Description', '-'))
  370. f.write(rstize_text(class_def.description.strip(), state) + "\n\n")
  371. # Online tutorials
  372. if len(class_def.tutorials) > 0:
  373. f.write(make_heading('Tutorials', '-'))
  374. for t in class_def.tutorials:
  375. link = t.strip()
  376. match = GODOT_DOCS_PATTERN.search(link)
  377. if match:
  378. groups = match.groups()
  379. if match.lastindex == 2:
  380. # Doc reference with fragment identifier: emit direct link to section with reference to page, for example:
  381. # `#calling-javascript-from-script in Exporting For Web`
  382. f.write("- `" + groups[1] + " <../" + groups[0] + ".html" + groups[1] + ">`_ in :doc:`../" + groups[0] + "`\n\n")
  383. # Commented out alternative: Instead just emit:
  384. # `Subsection in Exporting For Web`
  385. # f.write("- `Subsection <../" + groups[0] + ".html" + groups[1] + ">`_ in :doc:`../" + groups[0] + "`\n\n")
  386. elif match.lastindex == 1:
  387. # Doc reference, for example:
  388. # `Math`
  389. f.write("- :doc:`../" + groups[0] + "`\n\n")
  390. else:
  391. # External link, for example:
  392. # `http://enet.bespin.org/usergroup0.html`
  393. f.write("- `" + link + " <" + link + ">`_\n\n")
  394. # Property descriptions
  395. if len(class_def.properties) > 0:
  396. f.write(make_heading('Property Descriptions', '-'))
  397. for property_def in class_def.properties.values():
  398. #f.write(".. _class_{}_{}:\n\n".format(class_name, property_def.name))
  399. f.write(".. _class_{}_property_{}:\n\n".format(class_name, property_def.name))
  400. f.write('- {} **{}**\n\n'.format(property_def.type_name.to_rst(state), property_def.name))
  401. setget = []
  402. if property_def.setter is not None and not property_def.setter.startswith("_"):
  403. setget.append(("*Setter*", property_def.setter + '(value)'))
  404. if property_def.getter is not None and not property_def.getter.startswith("_"):
  405. setget.append(('*Getter*', property_def.getter + '()'))
  406. if len(setget) > 0:
  407. format_table(f, setget)
  408. if property_def.text is not None and property_def.text.strip() != '':
  409. f.write(rstize_text(property_def.text.strip(), state))
  410. f.write('\n\n')
  411. # Method descriptions
  412. if len(class_def.methods) > 0:
  413. f.write(make_heading('Method Descriptions', '-'))
  414. for method_list in class_def.methods.values():
  415. for i, m in enumerate(method_list):
  416. if i == 0:
  417. #f.write(".. _class_{}_{}:\n\n".format(class_name, m.name))
  418. f.write(".. _class_{}_method_{}:\n\n".format(class_name, m.name))
  419. ret_type, signature = make_method_signature(class_def, m, False, state)
  420. f.write("- {} {}\n\n".format(ret_type, signature))
  421. if m.description is None or m.description.strip() == '':
  422. continue
  423. f.write(rstize_text(m.description.strip(), state))
  424. f.write("\n\n")
  425. def make_class_list(class_list, columns): # type: (List[str], int) -> None
  426. # This function is no longer used.
  427. f = open('class_list.rst', 'w', encoding='utf-8')
  428. col_max = len(class_list) // columns + 1
  429. print(('col max is ', col_max))
  430. fit_columns = [] # type: List[List[str]]
  431. for _ in range(0, columns):
  432. fit_columns.append([])
  433. indexers = [] # type List[str]
  434. last_initial = ''
  435. for idx, name in enumerate(class_list):
  436. col = idx // col_max
  437. if col >= columns:
  438. col = columns - 1
  439. fit_columns[col].append(name)
  440. idx += 1
  441. if name[:1] != last_initial:
  442. indexers.append(name)
  443. last_initial = name[:1]
  444. row_max = 0
  445. f.write("\n")
  446. for n in range(0, columns):
  447. if len(fit_columns[n]) > row_max:
  448. row_max = len(fit_columns[n])
  449. f.write("| ")
  450. for n in range(0, columns):
  451. f.write(" | |")
  452. f.write("\n")
  453. f.write("+")
  454. for n in range(0, columns):
  455. f.write("--+-------+")
  456. f.write("\n")
  457. for r in range(0, row_max):
  458. s = '+ '
  459. for c in range(0, columns):
  460. if r >= len(fit_columns[c]):
  461. continue
  462. classname = fit_columns[c][r]
  463. initial = classname[0]
  464. if classname in indexers:
  465. s += '**' + initial + '** | '
  466. else:
  467. s += ' | '
  468. s += '[' + classname + '](class_' + classname.lower() + ') | '
  469. s += '\n'
  470. f.write(s)
  471. for n in range(0, columns):
  472. f.write("--+-------+")
  473. f.write("\n")
  474. f.close()
  475. def rstize_text(text, state): # type: (str, State) -> str
  476. # Linebreak + tabs in the XML should become two line breaks unless in a "codeblock"
  477. pos = 0
  478. while True:
  479. pos = text.find('\n', pos)
  480. if pos == -1:
  481. break
  482. pre_text = text[:pos]
  483. while text[pos + 1] == '\t':
  484. pos += 1
  485. post_text = text[pos + 1:]
  486. # Handle codeblocks
  487. if post_text.startswith("[codeblock]"):
  488. end_pos = post_text.find("[/codeblock]")
  489. if end_pos == -1:
  490. print_error("[codeblock] without a closing tag, file: {}".format(state.current_class), state)
  491. return ""
  492. code_text = post_text[len("[codeblock]"):end_pos]
  493. post_text = post_text[end_pos:]
  494. # Remove extraneous tabs
  495. code_pos = 0
  496. while True:
  497. code_pos = code_text.find('\n', code_pos)
  498. if code_pos == -1:
  499. break
  500. to_skip = 0
  501. while code_pos + to_skip + 1 < len(code_text) and code_text[code_pos + to_skip + 1] == '\t':
  502. to_skip += 1
  503. if len(code_text[code_pos + to_skip + 1:]) == 0:
  504. code_text = code_text[:code_pos] + "\n"
  505. code_pos += 1
  506. else:
  507. code_text = code_text[:code_pos] + "\n " + code_text[code_pos + to_skip + 1:]
  508. code_pos += 5 - to_skip
  509. text = pre_text + "\n[codeblock]" + code_text + post_text
  510. pos += len("\n[codeblock]" + code_text)
  511. # Handle normal text
  512. else:
  513. text = pre_text + "\n\n" + post_text
  514. pos += 2
  515. next_brac_pos = text.find('[')
  516. # Escape \ character, otherwise it ends up as an escape character in rst
  517. pos = 0
  518. while True:
  519. pos = text.find('\\', pos, next_brac_pos)
  520. if pos == -1:
  521. break
  522. text = text[:pos] + "\\\\" + text[pos + 1:]
  523. pos += 2
  524. # Escape * character to avoid interpreting it as emphasis
  525. pos = 0
  526. while True:
  527. pos = text.find('*', pos, next_brac_pos)
  528. if pos == -1:
  529. break
  530. text = text[:pos] + "\*" + text[pos + 1:]
  531. pos += 2
  532. # Escape _ character at the end of a word to avoid interpreting it as an inline hyperlink
  533. pos = 0
  534. while True:
  535. pos = text.find('_', pos, next_brac_pos)
  536. if pos == -1:
  537. break
  538. if not text[pos + 1].isalnum(): # don't escape within a snake_case word
  539. text = text[:pos] + "\_" + text[pos + 1:]
  540. pos += 2
  541. else:
  542. pos += 1
  543. # Handle [tags]
  544. inside_code = False
  545. pos = 0
  546. tag_depth = 0
  547. while True:
  548. pos = text.find('[', pos)
  549. if pos == -1:
  550. break
  551. endq_pos = text.find(']', pos + 1)
  552. if endq_pos == -1:
  553. break
  554. pre_text = text[:pos]
  555. post_text = text[endq_pos + 1:]
  556. tag_text = text[pos + 1:endq_pos]
  557. escape_post = False
  558. if tag_text in state.classes:
  559. if tag_text == state.current_class:
  560. # We don't want references to the same class
  561. tag_text = '``{}``'.format(tag_text)
  562. else:
  563. tag_text = make_type(tag_text, state)
  564. escape_post = True
  565. else: # command
  566. cmd = tag_text
  567. space_pos = tag_text.find(' ')
  568. if cmd == '/codeblock':
  569. tag_text = ''
  570. tag_depth -= 1
  571. inside_code = False
  572. # Strip newline if the tag was alone on one
  573. if pre_text[-1] == '\n':
  574. pre_text = pre_text[:-1]
  575. elif cmd == '/code':
  576. tag_text = '``'
  577. tag_depth -= 1
  578. inside_code = False
  579. escape_post = True
  580. elif inside_code:
  581. tag_text = '[' + tag_text + ']'
  582. elif cmd.find('html') == 0:
  583. param = tag_text[space_pos + 1:]
  584. tag_text = param
  585. elif cmd.startswith('method') or cmd.startswith('member') or cmd.startswith('signal') or cmd.startswith('constant'):
  586. param = tag_text[space_pos + 1:]
  587. if param.find('.') != -1:
  588. ss = param.split('.')
  589. if len(ss) > 2:
  590. print_error("Bad reference: '{}', file: {}".format(param, state.current_class), state)
  591. class_param, method_param = ss
  592. else:
  593. class_param = state.current_class
  594. method_param = param
  595. ref_type = ""
  596. if class_param in state.classes:
  597. class_def = state.classes[class_param]
  598. if cmd.startswith("method"):
  599. if method_param not in class_def.methods:
  600. print_error("Unresolved method '{}', file: {}".format(param, state.current_class), state)
  601. ref_type = "_method"
  602. elif cmd.startswith("member"):
  603. if method_param not in class_def.properties:
  604. print_error("Unresolved member '{}', file: {}".format(param, state.current_class), state)
  605. ref_type = "_property"
  606. elif cmd.startswith("signal"):
  607. if method_param not in class_def.signals:
  608. print_error("Unresolved signal '{}', file: {}".format(param, state.current_class), state)
  609. ref_type = "_signal"
  610. elif cmd.startswith("constant"):
  611. found = False
  612. # Search in the current class
  613. search_class_defs = [class_def]
  614. if param.find('.') == -1:
  615. # Also search in @GlobalScope as a last resort if no class was specified
  616. search_class_defs.append(state.classes["@GlobalScope"])
  617. for search_class_def in search_class_defs:
  618. if method_param in search_class_def.constants:
  619. class_param = search_class_def.name
  620. found = True
  621. else:
  622. for enum in search_class_def.enums.values():
  623. if method_param in enum.values:
  624. class_param = search_class_def.name
  625. found = True
  626. break
  627. if not found:
  628. print_error("Unresolved constant '{}', file: {}".format(param, state.current_class), state)
  629. ref_type = "_constant"
  630. else:
  631. print_error("Unresolved type reference '{}' in method reference '{}', file: {}".format(class_param, param, state.current_class), state)
  632. repl_text = method_param
  633. if class_param != state.current_class:
  634. repl_text = "{}.{}".format(class_param, method_param)
  635. tag_text = ':ref:`{}<class_{}{}_{}>`'.format(repl_text, class_param, ref_type, method_param)
  636. escape_post = True
  637. elif cmd.find('image=') == 0:
  638. tag_text = "" # '![](' + cmd[6:] + ')'
  639. elif cmd.find('url=') == 0:
  640. tag_text = ':ref:`' + cmd[4:] + '<' + cmd[4:] + ">`"
  641. tag_depth += 1
  642. elif cmd == '/url':
  643. tag_text = ''
  644. tag_depth -= 1
  645. escape_post = True
  646. elif cmd == 'center':
  647. tag_depth += 1
  648. tag_text = ''
  649. elif cmd == '/center':
  650. tag_depth -= 1
  651. tag_text = ''
  652. elif cmd == 'codeblock':
  653. tag_depth += 1
  654. tag_text = '\n::\n'
  655. inside_code = True
  656. elif cmd == 'br':
  657. # Make a new paragraph instead of a linebreak, rst is not so linebreak friendly
  658. tag_text = '\n\n'
  659. # Strip potential leading spaces
  660. while post_text[0] == ' ':
  661. post_text = post_text[1:]
  662. elif cmd == 'i' or cmd == '/i':
  663. if cmd == "/i":
  664. tag_depth -= 1
  665. else:
  666. tag_depth += 1
  667. tag_text = '*'
  668. elif cmd == 'b' or cmd == '/b':
  669. if cmd == "/b":
  670. tag_depth -= 1
  671. else:
  672. tag_depth += 1
  673. tag_text = '**'
  674. elif cmd == 'u' or cmd == '/u':
  675. if cmd == "/u":
  676. tag_depth -= 1
  677. else:
  678. tag_depth += 1
  679. tag_text = ''
  680. elif cmd == 'code':
  681. tag_text = '``'
  682. tag_depth += 1
  683. inside_code = True
  684. elif cmd.startswith('enum '):
  685. tag_text = make_enum(cmd[5:], state)
  686. else:
  687. tag_text = make_type(tag_text, state)
  688. escape_post = True
  689. # Properly escape things like `[Node]s`
  690. if escape_post and post_text and (post_text[0].isalnum() or post_text[0] == "("): # not punctuation, escape
  691. post_text = '\ ' + post_text
  692. next_brac_pos = post_text.find('[', 0)
  693. iter_pos = 0
  694. while not inside_code:
  695. iter_pos = post_text.find('*', iter_pos, next_brac_pos)
  696. if iter_pos == -1:
  697. break
  698. post_text = post_text[:iter_pos] + "\*" + post_text[iter_pos + 1:]
  699. iter_pos += 2
  700. iter_pos = 0
  701. while not inside_code:
  702. iter_pos = post_text.find('_', iter_pos, next_brac_pos)
  703. if iter_pos == -1:
  704. break
  705. if not post_text[iter_pos + 1].isalnum(): # don't escape within a snake_case word
  706. post_text = post_text[:iter_pos] + "\_" + post_text[iter_pos + 1:]
  707. iter_pos += 2
  708. else:
  709. iter_pos += 1
  710. text = pre_text + tag_text + post_text
  711. pos = len(pre_text) + len(tag_text)
  712. if tag_depth > 0:
  713. print_error("Tag depth mismatch: too many/little open/close tags, file: {}".format(state.current_class), state)
  714. return text
  715. def format_table(f, pp): # type: (TextIO, Iterable[Tuple[str, ...]]) -> None
  716. longest_t = 0
  717. longest_s = 0
  718. for s in pp:
  719. sl = len(s[0])
  720. if sl > longest_s:
  721. longest_s = sl
  722. tl = len(s[1])
  723. if tl > longest_t:
  724. longest_t = tl
  725. sep = "+"
  726. for i in range(longest_s + 2):
  727. sep += "-"
  728. sep += "+"
  729. for i in range(longest_t + 2):
  730. sep += "-"
  731. sep += "+\n"
  732. f.write(sep)
  733. for s in pp:
  734. rt = s[0]
  735. while len(rt) < longest_s:
  736. rt += " "
  737. st = s[1]
  738. while len(st) < longest_t:
  739. st += " "
  740. f.write("| " + rt + " | " + st + " |\n")
  741. f.write(sep)
  742. f.write('\n')
  743. def make_type(t, state): # type: (str, State) -> str
  744. if t in state.classes:
  745. return ':ref:`{0}<class_{0}>`'.format(t)
  746. print_error("Unresolved type '{}', file: {}".format(t, state.current_class), state)
  747. return t
  748. def make_enum(t, state): # type: (str, State) -> str
  749. p = t.find(".")
  750. if p >= 0:
  751. c = t[0:p]
  752. e = t[p + 1:]
  753. # Variant enums live in GlobalScope but still use periods.
  754. if c == "Variant":
  755. c = "@GlobalScope"
  756. e = "Variant." + e
  757. else:
  758. c = state.current_class
  759. e = t
  760. if c in state.classes and e not in state.classes[c].enums:
  761. c = "@GlobalScope"
  762. if not c in state.classes and c.startswith("_"):
  763. c = c[1:] # Remove the underscore prefix
  764. if c in state.classes and e in state.classes[c].enums:
  765. return ":ref:`{0}<enum_{1}_{0}>`".format(e, c)
  766. print_error("Unresolved enum '{}', file: {}".format(t, state.current_class), state)
  767. return t
  768. def make_method_signature(class_def, method_def, make_ref, state): # type: (ClassDef, Union[MethodDef, SignalDef], bool, State) -> Tuple[str, str]
  769. ret_type = " "
  770. ref_type = "signal"
  771. if isinstance(method_def, MethodDef):
  772. ret_type = method_def.return_type.to_rst(state)
  773. ref_type = "method"
  774. out = ""
  775. if make_ref:
  776. out += ":ref:`{0}<class_{1}_{2}_{0}>` ".format(method_def.name, class_def.name, ref_type)
  777. else:
  778. out += "**{}** ".format(method_def.name)
  779. out += '**(**'
  780. for i, arg in enumerate(method_def.parameters):
  781. if i > 0:
  782. out += ', '
  783. else:
  784. out += ' '
  785. out += "{} {}".format(arg.type_name.to_rst(state), arg.name)
  786. if arg.default_value is not None:
  787. out += '=' + arg.default_value
  788. if isinstance(method_def, MethodDef) and method_def.qualifiers is not None and 'vararg' in method_def.qualifiers:
  789. if len(method_def.parameters) > 0:
  790. out += ', ...'
  791. else:
  792. out += ' ...'
  793. out += ' **)**'
  794. if isinstance(method_def, MethodDef) and method_def.qualifiers is not None:
  795. out += ' ' + method_def.qualifiers
  796. return ret_type, out
  797. def make_heading(title, underline): # type: (str, str) -> str
  798. return title + '\n' + (underline * len(title)) + "\n\n"
  799. if __name__ == '__main__':
  800. main()