summaryrefslogtreecommitdiff
path: root/doc/tools/makerst.py
diff options
context:
space:
mode:
Diffstat (limited to 'doc/tools/makerst.py')
-rwxr-xr-xdoc/tools/makerst.py285
1 files changed, 178 insertions, 107 deletions
diff --git a/doc/tools/makerst.py b/doc/tools/makerst.py
index e81b4db13e..91240e9550 100755
--- a/doc/tools/makerst.py
+++ b/doc/tools/makerst.py
@@ -14,7 +14,7 @@ GODOT_DOCS_PATTERN = re.compile(r'^http(?:s)?://docs\.godotengine\.org/(?:[a-zA-
def print_error(error, state): # type: (str, State) -> None
- print(error)
+ print("ERROR: {}".format(error))
state.errored = True
@@ -37,12 +37,14 @@ class TypeName:
class PropertyDef:
- def __init__(self, name, type_name, setter, getter, text): # type: (str, TypeName, Optional[str], Optional[str], Optional[str]) -> None
+ def __init__(self, name, type_name, setter, getter, text, default_value, overridden): # type: (str, TypeName, Optional[str], Optional[str], Optional[str], Optional[str], Optional[bool]) -> None
self.name = name
self.type_name = type_name
self.setter = setter
self.getter = getter
self.text = text
+ self.default_value = default_value
+ self.overridden = overridden
class ParameterDef:
def __init__(self, name, type_name, default_value): # type: (str, TypeName, Optional[str]) -> None
@@ -81,9 +83,10 @@ class EnumDef:
class ThemeItemDef:
- def __init__(self, name, type_name): # type: (str, TypeName) -> None
+ def __init__(self, name, type_name, default_value): # type: (str, TypeName, Optional[str]) -> None
self.name = name
self.type_name = type_name
+ self.default_value = default_value
class ClassDef:
@@ -144,8 +147,12 @@ class State:
type_name = TypeName.from_element(property)
setter = property.get("setter") or None # Use or None so '' gets turned into None.
getter = property.get("getter") or None
+ default_value = property.get("default") or None
+ if default_value is not None:
+ default_value = escape_rst(default_value)
+ overridden = property.get("override") or False
- property_def = PropertyDef(property_name, type_name, setter, getter, property.text)
+ property_def = PropertyDef(property_name, type_name, setter, getter, property.text, default_value, overridden)
class_def.properties[property_name] = property_def
methods = class_root.find("methods")
@@ -230,7 +237,8 @@ class State:
assert theme_item.tag == "theme_item"
theme_item_name = theme_item.attrib["name"]
- theme_item_def = ThemeItemDef(theme_item_name, TypeName.from_element(theme_item))
+ default_value = theme_item.get("default") or None
+ theme_item_def = ThemeItemDef(theme_item_name, TypeName.from_element(theme_item), default_value)
if theme_item_name not in class_def.theme_items:
class_def.theme_items[theme_item_name] = []
class_def.theme_items[theme_item_name].append(theme_item_def)
@@ -343,6 +351,7 @@ def make_rst_class(class_def, state, dry_run, output_dir): # type: (ClassDef, S
f = open(os.path.join(output_dir, "class_" + class_name.lower() + '.rst'), 'w', encoding='utf-8')
# Warn contributors not to edit this file directly
+ f.write(":github_url: hide\n\n")
f.write(".. Generated automatically by doc/tools/makerst.py in Godot's source tree.\n")
f.write(".. DO NOT EDIT THIS FILE, but the " + class_name + ".xml source instead.\n")
f.write(".. The source is found in doc/classes or modules/<name>/doc_classes.\n\n")
@@ -396,12 +405,16 @@ def make_rst_class(class_def, state, dry_run, output_dir): # type: (ClassDef, S
# Properties overview
if len(class_def.properties) > 0:
f.write(make_heading('Properties', '-'))
- ml = [] # type: List[Tuple[str, str]]
+ ml = [] # type: List[Tuple[str, str, str]]
for property_def in class_def.properties.values():
type_rst = property_def.type_name.to_rst(state)
- ref = ":ref:`{0}<class_{1}_property_{0}>`".format(property_def.name, class_name)
- ml.append((type_rst, ref))
- format_table(f, ml)
+ default = property_def.default_value
+ if property_def.overridden:
+ ml.append((type_rst, property_def.name, "**O:** " + default))
+ else:
+ ref = ":ref:`{0}<class_{1}_property_{0}>`".format(property_def.name, class_name)
+ ml.append((type_rst, ref, default))
+ format_table(f, ml, True)
# Methods overview
if len(class_def.methods) > 0:
@@ -415,30 +428,39 @@ def make_rst_class(class_def, state, dry_run, output_dir): # type: (ClassDef, S
# Theme properties
if class_def.theme_items is not None and len(class_def.theme_items) > 0:
f.write(make_heading('Theme Properties', '-'))
- ml = []
+ pl = []
for theme_item_list in class_def.theme_items.values():
for theme_item in theme_item_list:
- ml.append((theme_item.type_name.to_rst(state), theme_item.name))
- format_table(f, ml)
+ pl.append((theme_item.type_name.to_rst(state), theme_item.name, theme_item.default_value))
+ format_table(f, pl, True)
# Signals
if len(class_def.signals) > 0:
f.write(make_heading('Signals', '-'))
+ index = 0
+
for signal in class_def.signals.values():
- #f.write(".. _class_{}_{}:\n\n".format(class_name, signal.name))
+ if index != 0:
+ f.write('----\n\n')
+
f.write(".. _class_{}_signal_{}:\n\n".format(class_name, signal.name))
_, signature = make_method_signature(class_def, signal, False, state)
f.write("- {}\n\n".format(signature))
- if signal.description is None or signal.description.strip() == '':
- continue
- f.write(rstize_text(signal.description.strip(), state))
- f.write("\n\n")
+ if signal.description is not None and signal.description.strip() != '':
+ f.write(rstize_text(signal.description.strip(), state) + '\n\n')
+
+ index += 1
# Enums
if len(class_def.enums) > 0:
f.write(make_heading('Enumerations', '-'))
+ index = 0
+
for e in class_def.enums.values():
+ if index != 0:
+ f.write('----\n\n')
+
f.write(".. _enum_{}_{}:\n\n".format(class_name, e.name))
# Sphinx seems to divide the bullet list into individual <ul> tags if we weave the labels into it.
# As such I'll put them all above the list. Won't be perfect but better than making the list visually broken.
@@ -452,8 +474,11 @@ def make_rst_class(class_def, state, dry_run, output_dir): # type: (ClassDef, S
f.write("- **{}** = **{}**".format(value.name, value.value))
if value.text is not None and value.text.strip() != '':
f.write(' --- ' + rstize_text(value.text.strip(), state))
+
f.write('\n\n')
+ index += 1
+
# Constants
if len(class_def.constants) > 0:
f.write(make_heading('Constants', '-'))
@@ -466,6 +491,7 @@ def make_rst_class(class_def, state, dry_run, output_dir): # type: (ClassDef, S
f.write("- **{}** = **{}**".format(constant.name, constant.value))
if constant.text is not None and constant.text.strip() != '':
f.write(' --- ' + rstize_text(constant.text.strip(), state))
+
f.write('\n\n')
# Class description
@@ -478,61 +504,59 @@ def make_rst_class(class_def, state, dry_run, output_dir): # type: (ClassDef, S
f.write(make_heading('Tutorials', '-'))
for t in class_def.tutorials:
link = t.strip()
- match = GODOT_DOCS_PATTERN.search(link)
- if match:
- groups = match.groups()
- if match.lastindex == 2:
- # Doc reference with fragment identifier: emit direct link to section with reference to page, for example:
- # `#calling-javascript-from-script in Exporting For Web`
- f.write("- `" + groups[1] + " <../" + groups[0] + ".html" + groups[1] + ">`_ in :doc:`../" + groups[0] + "`\n\n")
- # Commented out alternative: Instead just emit:
- # `Subsection in Exporting For Web`
- # f.write("- `Subsection <../" + groups[0] + ".html" + groups[1] + ">`_ in :doc:`../" + groups[0] + "`\n\n")
- elif match.lastindex == 1:
- # Doc reference, for example:
- # `Math`
- f.write("- :doc:`../" + groups[0] + "`\n\n")
- else:
- # External link, for example:
- # `http://enet.bespin.org/usergroup0.html`
- f.write("- `" + link + " <" + link + ">`_\n\n")
+ f.write("- " + make_url(link) + "\n\n")
# Property descriptions
- if len(class_def.properties) > 0:
+ if any(not p.overridden for p in class_def.properties.values()) > 0:
f.write(make_heading('Property Descriptions', '-'))
+ index = 0
+
for property_def in class_def.properties.values():
- #f.write(".. _class_{}_{}:\n\n".format(class_name, property_def.name))
+ if property_def.overridden:
+ continue
+
+ if index != 0:
+ f.write('----\n\n')
+
f.write(".. _class_{}_property_{}:\n\n".format(class_name, property_def.name))
f.write('- {} **{}**\n\n'.format(property_def.type_name.to_rst(state), property_def.name))
- setget = []
+ info = []
+ if property_def.default_value is not None:
+ info.append(("*Default*", property_def.default_value))
if property_def.setter is not None and not property_def.setter.startswith("_"):
- setget.append(("*Setter*", property_def.setter + '(value)'))
+ info.append(("*Setter*", property_def.setter + '(value)'))
if property_def.getter is not None and not property_def.getter.startswith("_"):
- setget.append(('*Getter*', property_def.getter + '()'))
+ info.append(('*Getter*', property_def.getter + '()'))
- if len(setget) > 0:
- format_table(f, setget)
+ if len(info) > 0:
+ format_table(f, info)
if property_def.text is not None and property_def.text.strip() != '':
- f.write(rstize_text(property_def.text.strip(), state))
- f.write('\n\n')
+ f.write(rstize_text(property_def.text.strip(), state) + '\n\n')
+
+ index += 1
# Method descriptions
if len(class_def.methods) > 0:
f.write(make_heading('Method Descriptions', '-'))
+ index = 0
+
for method_list in class_def.methods.values():
for i, m in enumerate(method_list):
+ if index != 0:
+ f.write('----\n\n')
+
if i == 0:
- #f.write(".. _class_{}_{}:\n\n".format(class_name, m.name))
f.write(".. _class_{}_method_{}:\n\n".format(class_name, m.name))
+
ret_type, signature = make_method_signature(class_def, m, False, state)
f.write("- {} {}\n\n".format(ret_type, signature))
- if m.description is None or m.description.strip() == '':
- continue
- f.write(rstize_text(m.description.strip(), state))
- f.write("\n\n")
+ if m.description is not None and m.description.strip() != '':
+ f.write(rstize_text(m.description.strip(), state) + '\n\n')
+
+ index += 1
def make_class_list(class_list, columns): # type: (List[str], int) -> None
@@ -600,6 +624,40 @@ def make_class_list(class_list, columns): # type: (List[str], int) -> None
f.close()
+def escape_rst(text, until_pos=-1): # type: (str) -> str
+ # Escape \ character, otherwise it ends up as an escape character in rst
+ pos = 0
+ while True:
+ pos = text.find('\\', pos, until_pos)
+ if pos == -1:
+ break
+ text = text[:pos] + "\\\\" + text[pos + 1:]
+ pos += 2
+
+ # Escape * character to avoid interpreting it as emphasis
+ pos = 0
+ while True:
+ pos = text.find('*', pos, until_pos)
+ if pos == -1:
+ break
+ text = text[:pos] + "\*" + text[pos + 1:]
+ pos += 2
+
+ # Escape _ character at the end of a word to avoid interpreting it as an inline hyperlink
+ pos = 0
+ while True:
+ pos = text.find('_', pos, until_pos)
+ if pos == -1:
+ break
+ if not text[pos + 1].isalnum(): # don't escape within a snake_case word
+ text = text[:pos] + "\_" + text[pos + 1:]
+ pos += 2
+ else:
+ pos += 1
+
+ return text
+
+
def rstize_text(text, state): # type: (str, State) -> str
# Linebreak + tabs in the XML should become two line breaks unless in a "codeblock"
pos = 0
@@ -609,8 +667,10 @@ def rstize_text(text, state): # type: (str, State) -> str
break
pre_text = text[:pos]
+ indent_level = 0
while text[pos + 1] == '\t':
pos += 1
+ indent_level += 1
post_text = text[pos + 1:]
# Handle codeblocks
@@ -634,6 +694,9 @@ def rstize_text(text, state): # type: (str, State) -> str
while code_pos + to_skip + 1 < len(code_text) and code_text[code_pos + to_skip + 1] == '\t':
to_skip += 1
+ if to_skip > indent_level:
+ print_error("Four spaces should be used for indentation within [codeblock], file: {}".format(state.current_class), state)
+
if len(code_text[code_pos + to_skip + 1:]) == 0:
code_text = code_text[:code_pos] + "\n"
code_pos += 1
@@ -650,43 +713,20 @@ def rstize_text(text, state): # type: (str, State) -> str
pos += 2
next_brac_pos = text.find('[')
-
- # Escape \ character, otherwise it ends up as an escape character in rst
- pos = 0
- while True:
- pos = text.find('\\', pos, next_brac_pos)
- if pos == -1:
- break
- text = text[:pos] + "\\\\" + text[pos + 1:]
- pos += 2
-
- # Escape * character to avoid interpreting it as emphasis
- pos = 0
- while True:
- pos = text.find('*', pos, next_brac_pos)
- if pos == -1:
- break
- text = text[:pos] + "\*" + text[pos + 1:]
- pos += 2
-
- # Escape _ character at the end of a word to avoid interpreting it as an inline hyperlink
- pos = 0
- while True:
- pos = text.find('_', pos, next_brac_pos)
- if pos == -1:
- break
- if not text[pos + 1].isalnum(): # don't escape within a snake_case word
- text = text[:pos] + "\_" + text[pos + 1:]
- pos += 2
- else:
- pos += 1
+ text = escape_rst(text, next_brac_pos)
# Handle [tags]
inside_code = False
+ inside_url = False
+ url_has_name = False
+ url_link = ""
pos = 0
tag_depth = 0
+ previous_pos = 0
while True:
pos = text.find('[', pos)
+ if inside_url and (pos > previous_pos):
+ url_has_name = True
if pos == -1:
break
@@ -795,12 +835,17 @@ def rstize_text(text, state): # type: (str, State) -> str
elif cmd.find('image=') == 0:
tag_text = "" # '![](' + cmd[6:] + ')'
elif cmd.find('url=') == 0:
- tag_text = ':ref:`' + cmd[4:] + '<' + cmd[4:] + ">`"
+ url_link = cmd[4:]
+ tag_text = '`'
tag_depth += 1
+ inside_url = True
+ url_has_name = False
elif cmd == '/url':
- tag_text = ''
+ tag_text = ('' if url_has_name else url_link) + " <" + url_link + ">`_"
tag_depth -= 1
escape_post = True
+ inside_url = False
+ url_has_name = False
elif cmd == 'center':
tag_depth += 1
tag_text = ''
@@ -871,6 +916,7 @@ def rstize_text(text, state): # type: (str, State) -> str
text = pre_text + tag_text + post_text
pos = len(pre_text) + len(tag_text)
+ previous_pos = pos
if tag_depth > 0:
print_error("Tag depth mismatch: too many/little open/close tags, file: {}".format(state.current_class), state)
@@ -878,33 +924,33 @@ def rstize_text(text, state): # type: (str, State) -> str
return text
-def format_table(f, pp): # type: (TextIO, Iterable[Tuple[str, ...]]) -> None
- longest_t = 0
- longest_s = 0
- for s in pp:
- sl = len(s[0])
- if sl > longest_s:
- longest_s = sl
- tl = len(s[1])
- if tl > longest_t:
- longest_t = tl
-
- sep = "+"
- for i in range(longest_s + 2):
- sep += "-"
- sep += "+"
- for i in range(longest_t + 2):
- sep += "-"
+def format_table(f, data, remove_empty_columns=False): # type: (TextIO, Iterable[Tuple[str, ...]]) -> None
+ if len(data) == 0:
+ return
+
+ column_sizes = [0] * len(data[0])
+ for row in data:
+ for i, text in enumerate(row):
+ text_length = len(text or '')
+ if text_length > column_sizes[i]:
+ column_sizes[i] = text_length
+
+ sep = ""
+ for size in column_sizes:
+ if size == 0 and remove_empty_columns:
+ continue
+ sep += "+" + "-" * (size + 2)
sep += "+\n"
f.write(sep)
- for s in pp:
- rt = s[0]
- while len(rt) < longest_s:
- rt += " "
- st = s[1]
- while len(st) < longest_t:
- st += " "
- f.write("| " + rt + " | " + st + " |\n")
+
+ for row in data:
+ row_text = "|"
+ for i, text in enumerate(row):
+ if column_sizes[i] == 0 and remove_empty_columns:
+ continue
+ row_text += " " + (text or '').ljust(column_sizes[i]) + " |"
+ row_text += "\n"
+ f.write(row_text)
f.write(sep)
f.write('\n')
@@ -936,7 +982,11 @@ def make_enum(t, state): # type: (str, State) -> str
if c in state.classes and e in state.classes[c].enums:
return ":ref:`{0}<enum_{1}_{0}>`".format(e, c)
- print_error("Unresolved enum '{}', file: {}".format(t, state.current_class), state)
+
+ # Don't fail for `Vector3.Axis`, as this enum is a special case which is expected not to be resolved.
+ if "{}.{}".format(c, e) != "Vector3.Axis":
+ print_error("Unresolved enum '{}', file: {}".format(t, state.current_class), state)
+
return t
@@ -985,5 +1035,26 @@ def make_heading(title, underline): # type: (str, str) -> str
return title + '\n' + (underline * len(title)) + "\n\n"
+def make_url(link): # type: (str) -> str
+ match = GODOT_DOCS_PATTERN.search(link)
+ if match:
+ groups = match.groups()
+ if match.lastindex == 2:
+ # Doc reference with fragment identifier: emit direct link to section with reference to page, for example:
+ # `#calling-javascript-from-script in Exporting For Web`
+ return "`" + groups[1] + " <../" + groups[0] + ".html" + groups[1] + ">`_ in :doc:`../" + groups[0] + "`"
+ # Commented out alternative: Instead just emit:
+ # `Subsection in Exporting For Web`
+ # return "`Subsection <../" + groups[0] + ".html" + groups[1] + ">`__ in :doc:`../" + groups[0] + "`"
+ elif match.lastindex == 1:
+ # Doc reference, for example:
+ # `Math`
+ return ":doc:`../" + groups[0] + "`"
+ else:
+ # External link, for example:
+ # `http://enet.bespin.org/usergroup0.html`
+ return "`" + link + " <" + link + ">`_"
+
+
if __name__ == '__main__':
main()