summaryrefslogtreecommitdiff
path: root/doc/tools
diff options
context:
space:
mode:
Diffstat (limited to 'doc/tools')
-rwxr-xr-xdoc/tools/make_rst.py (renamed from doc/tools/makerst.py)375
1 files changed, 315 insertions, 60 deletions
diff --git a/doc/tools/makerst.py b/doc/tools/make_rst.py
index ae3cc73098..b5e5cf8fa7 100755
--- a/doc/tools/makerst.py
+++ b/doc/tools/make_rst.py
@@ -1,5 +1,7 @@
#!/usr/bin/env python3
+# This script makes RST files from the XML class reference for use with the online docs.
+
import argparse
import os
import re
@@ -9,10 +11,8 @@ from collections import OrderedDict
# Uncomment to do type checks. I have it commented out so it works below Python 3.5
# from typing import List, Dict, TextIO, Tuple, Iterable, Optional, DefaultDict, Any, Union
-# http(s)://docs.godotengine.org/<langcode>/<tag>/path/to/page.html(#fragment-tag)
-GODOT_DOCS_PATTERN = re.compile(
- r"^http(?:s)?://docs\.godotengine\.org/(?:[a-zA-Z0-9.\-_]*)/(?:[a-zA-Z0-9.\-_]*)/(.*)\.html(#.*)?$"
-)
+# $DOCS_URL/path/to/page.html(#fragment-tag)
+GODOT_DOCS_PATTERN = re.compile(r"^\$DOCS_URL/(.*)\.html(#.*)?$")
def print_error(error, state): # type: (str, State) -> None
@@ -90,9 +90,13 @@ class EnumDef:
class ThemeItemDef:
- def __init__(self, name, type_name, default_value): # type: (str, TypeName, Optional[str]) -> None
+ def __init__(
+ self, name, type_name, data_name, text, default_value
+ ): # type: (str, TypeName, str, Optional[str], Optional[str]) -> None
self.name = name
self.type_name = type_name
+ self.data_name = data_name
+ self.text = text
self.default_value = default_value
@@ -102,13 +106,15 @@ class ClassDef:
self.constants = OrderedDict() # type: OrderedDict[str, ConstantDef]
self.enums = OrderedDict() # type: OrderedDict[str, EnumDef]
self.properties = OrderedDict() # type: OrderedDict[str, PropertyDef]
+ self.constructors = OrderedDict() # type: OrderedDict[str, List[MethodDef]]
self.methods = OrderedDict() # type: OrderedDict[str, List[MethodDef]]
+ self.operators = OrderedDict() # type: OrderedDict[str, List[MethodDef]]
self.signals = OrderedDict() # type: OrderedDict[str, SignalDef]
+ self.theme_items = OrderedDict() # type: OrderedDict[str, ThemeItemDef]
self.inherits = None # type: Optional[str]
self.brief_description = None # type: Optional[str]
self.description = None # type: Optional[str]
- self.theme_items = None # type: Optional[OrderedDict[str, List[ThemeItemDef]]]
- self.tutorials = [] # type: List[str]
+ self.tutorials = [] # type: List[Tuple[str, str]]
# Used to match the class with XML source for output filtering purposes.
self.filepath = "" # type: str
@@ -163,6 +169,34 @@ class State:
)
class_def.properties[property_name] = property_def
+ constructors = class_root.find("constructors")
+ if constructors is not None:
+ for constructor in constructors:
+ assert constructor.tag == "constructor"
+
+ method_name = constructor.attrib["name"]
+ qualifiers = constructor.get("qualifiers")
+
+ return_element = constructor.find("return")
+ if return_element is not None:
+ return_type = TypeName.from_element(return_element)
+
+ else:
+ return_type = TypeName("void")
+
+ params = parse_arguments(constructor)
+
+ desc_element = constructor.find("description")
+ method_desc = None
+ if desc_element is not None:
+ method_desc = desc_element.text
+
+ method_def = MethodDef(method_name, return_type, params, method_desc, qualifiers)
+ if method_name not in class_def.constructors:
+ class_def.constructors[method_name] = []
+
+ class_def.constructors[method_name].append(method_def)
+
methods = class_root.find("methods")
if methods is not None:
for method in methods:
@@ -191,6 +225,34 @@ class State:
class_def.methods[method_name].append(method_def)
+ operators = class_root.find("operators")
+ if operators is not None:
+ for operator in operators:
+ assert operator.tag == "operator"
+
+ method_name = operator.attrib["name"]
+ qualifiers = operator.get("qualifiers")
+
+ return_element = operator.find("return")
+ if return_element is not None:
+ return_type = TypeName.from_element(return_element)
+
+ else:
+ return_type = TypeName("void")
+
+ params = parse_arguments(operator)
+
+ desc_element = operator.find("description")
+ method_desc = None
+ if desc_element is not None:
+ method_desc = desc_element.text
+
+ method_def = MethodDef(method_name, return_type, params, method_desc, qualifiers)
+ if method_name not in class_def.operators:
+ class_def.operators[method_name] = []
+
+ class_def.operators[method_name].append(method_def)
+
constants = class_root.find("constants")
if constants is not None:
for constant in constants:
@@ -240,16 +302,33 @@ class State:
theme_items = class_root.find("theme_items")
if theme_items is not None:
- class_def.theme_items = OrderedDict()
for theme_item in theme_items:
assert theme_item.tag == "theme_item"
theme_item_name = theme_item.attrib["name"]
+ theme_item_data_name = theme_item.attrib["data_type"]
+ theme_item_id = "{}_{}".format(theme_item_data_name, theme_item_name)
+ if theme_item_id in class_def.theme_items:
+ print_error(
+ "Duplicate theme property '{}' of type '{}', file: {}".format(
+ theme_item_name, theme_item_data_name, class_name
+ ),
+ self,
+ )
+ continue
+
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)
+ if default_value is not None:
+ default_value = "``{}``".format(default_value)
+
+ theme_item_def = ThemeItemDef(
+ theme_item_name,
+ TypeName.from_element(theme_item),
+ theme_item_data_name,
+ theme_item.text,
+ default_value,
+ )
+ class_def.theme_items[theme_item_name] = theme_item_def
tutorials = class_root.find("tutorials")
if tutorials is not None:
@@ -257,7 +336,7 @@ class State:
assert link.tag == "link"
if link.text is not None:
- class_def.tutorials.append(link.text)
+ class_def.tutorials.append((link.text.strip(), link.get("title", "")))
def sort_classes(self): # type: () -> None
self.classes = OrderedDict(sorted(self.classes.items(), key=lambda t: t[0]))
@@ -350,6 +429,9 @@ def main(): # type: () -> None
pattern = re.compile(args.filter)
+ # Create the output folder recursively if it doesn't already exist.
+ os.makedirs(args.output, exist_ok=True)
+
for class_name, class_def in state.classes.items():
if args.filter and not pattern.search(class_def.filepath):
continue
@@ -358,6 +440,8 @@ def main(): # type: () -> None
if not state.errored:
print("No errors found.")
+ if not args.dry_run:
+ print("Wrote reStructuredText files for each class to: %s" % args.output)
else:
print("Errors were found in the class reference XML. Please check the messages above.")
exit(1)
@@ -373,7 +457,7 @@ def make_rst_class(class_def, state, dry_run, output_dir): # type: (ClassDef, S
# 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(".. Generated automatically by doc/tools/make_rst.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")
@@ -426,9 +510,8 @@ def make_rst_class(class_def, state, dry_run, output_dir): # type: (ClassDef, S
# Online tutorials
if len(class_def.tutorials) > 0:
f.write(make_heading("Tutorials", "-"))
- for t in class_def.tutorials:
- link = t.strip()
- f.write("- " + make_url(link) + "\n\n")
+ for url, title in class_def.tutorials:
+ f.write("- " + make_link(url, title) + "\n\n")
# Properties overview
if len(class_def.properties) > 0:
@@ -437,29 +520,47 @@ def make_rst_class(class_def, state, dry_run, output_dir): # type: (ClassDef, S
for property_def in class_def.properties.values():
type_rst = property_def.type_name.to_rst(state)
default = property_def.default_value
- if property_def.overridden:
+ if default is not None and property_def.overridden:
ml.append((type_rst, property_def.name, default + " *(parent override)*"))
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
+ # Constructors, Methods, Operators overview
+ if len(class_def.constructors) > 0:
+ f.write(make_heading("Constructors", "-"))
+ ml = []
+ for method_list in class_def.constructors.values():
+ for m in method_list:
+ ml.append(make_method_signature(class_def, m, "constructor", state))
+ format_table(f, ml)
+
if len(class_def.methods) > 0:
f.write(make_heading("Methods", "-"))
ml = []
for method_list in class_def.methods.values():
for m in method_list:
- ml.append(make_method_signature(class_def, m, True, state))
+ ml.append(make_method_signature(class_def, m, "method", state))
+ format_table(f, ml)
+
+ if len(class_def.operators) > 0:
+ f.write(make_heading("Operators", "-"))
+ ml = []
+ for method_list in class_def.operators.values():
+ for m in method_list:
+ ml.append(make_method_signature(class_def, m, "operator", state))
format_table(f, ml)
# Theme properties
- if class_def.theme_items is not None and len(class_def.theme_items) > 0:
+ if len(class_def.theme_items) > 0:
f.write(make_heading("Theme Properties", "-"))
pl = []
- for theme_item_list in class_def.theme_items.values():
- for theme_item in theme_item_list:
- pl.append((theme_item.type_name.to_rst(state), theme_item.name, theme_item.default_value))
+ for theme_item_def in class_def.theme_items.values():
+ ref = ":ref:`{0}<class_{2}_theme_{1}_{0}>`".format(
+ theme_item_def.name, theme_item_def.data_name, class_name
+ )
+ pl.append((theme_item_def.type_name.to_rst(state), ref, theme_item_def.default_value))
format_table(f, pl, True)
# Signals
@@ -472,7 +573,7 @@ def make_rst_class(class_def, state, dry_run, output_dir): # type: (ClassDef, S
f.write("----\n\n")
f.write(".. _class_{}_signal_{}:\n\n".format(class_name, signal.name))
- _, signature = make_method_signature(class_def, signal, False, state)
+ _, signature = make_method_signature(class_def, signal, "", state)
f.write("- {}\n\n".format(signature))
if signal.description is not None and signal.description.strip() != "":
@@ -553,7 +654,27 @@ def make_rst_class(class_def, state, dry_run, output_dir): # type: (ClassDef, S
index += 1
- # Method descriptions
+ # Constructor, Method, Operator descriptions
+ if len(class_def.constructors) > 0:
+ f.write(make_heading("Constructor Descriptions", "-"))
+ index = 0
+
+ for method_list in class_def.constructors.values():
+ for i, m in enumerate(method_list):
+ if index != 0:
+ f.write("----\n\n")
+
+ if i == 0:
+ f.write(".. _class_{}_constructor_{}:\n\n".format(class_name, m.name))
+
+ ret_type, signature = make_method_signature(class_def, m, "", state)
+ f.write("- {} {}\n\n".format(ret_type, signature))
+
+ if m.description is not None and m.description.strip() != "":
+ f.write(rstize_text(m.description.strip(), state) + "\n\n")
+
+ index += 1
+
if len(class_def.methods) > 0:
f.write(make_heading("Method Descriptions", "-"))
index = 0
@@ -566,7 +687,31 @@ def make_rst_class(class_def, state, dry_run, output_dir): # type: (ClassDef, S
if i == 0:
f.write(".. _class_{}_method_{}:\n\n".format(class_name, m.name))
- ret_type, signature = make_method_signature(class_def, m, False, state)
+ ret_type, signature = make_method_signature(class_def, m, "", state)
+ f.write("- {} {}\n\n".format(ret_type, signature))
+
+ if m.description is not None and m.description.strip() != "":
+ f.write(rstize_text(m.description.strip(), state) + "\n\n")
+
+ index += 1
+
+ if len(class_def.operators) > 0:
+ f.write(make_heading("Operator Descriptions", "-"))
+ index = 0
+
+ for method_list in class_def.operators.values():
+ for i, m in enumerate(method_list):
+ if index != 0:
+ f.write("----\n\n")
+
+ if i == 0:
+ f.write(
+ ".. _class_{}_operator_{}_{}:\n\n".format(
+ class_name, sanitize_operator_name(m.name, state), m.return_type.type_name
+ )
+ )
+
+ ret_type, signature = make_method_signature(class_def, m, "", state)
f.write("- {} {}\n\n".format(ret_type, signature))
if m.description is not None and m.description.strip() != "":
@@ -574,6 +719,30 @@ def make_rst_class(class_def, state, dry_run, output_dir): # type: (ClassDef, S
index += 1
+ # Theme property descriptions
+ if len(class_def.theme_items) > 0:
+ f.write(make_heading("Theme Property Descriptions", "-"))
+ index = 0
+
+ for theme_item_def in class_def.theme_items.values():
+ if index != 0:
+ f.write("----\n\n")
+
+ f.write(".. _class_{}_theme_{}_{}:\n\n".format(class_name, theme_item_def.data_name, theme_item_def.name))
+ f.write("- {} **{}**\n\n".format(theme_item_def.type_name.to_rst(state), theme_item_def.name))
+
+ info = []
+ if theme_item_def.default_value is not None:
+ info.append(("*Default*", theme_item_def.default_value))
+
+ if len(info) > 0:
+ format_table(f, info)
+
+ if theme_item_def.text is not None and theme_item_def.text.strip() != "":
+ f.write(rstize_text(theme_item_def.text.strip(), state) + "\n\n")
+
+ index += 1
+
f.write(make_footer())
@@ -686,16 +855,11 @@ def rstize_text(text, state): # type: (str, State) -> str
# 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
@@ -741,6 +905,7 @@ def rstize_text(text, state): # type: (str, State) -> str
or cmd.startswith("member")
or cmd.startswith("signal")
or cmd.startswith("constant")
+ or cmd.startswith("theme_item")
):
param = tag_text[space_pos + 1 :]
@@ -757,16 +922,33 @@ def rstize_text(text, state): # type: (str, State) -> str
ref_type = ""
if class_param in state.classes:
class_def = state.classes[class_param]
+ if cmd.startswith("constructor"):
+ if method_param not in class_def.constructors:
+ print_error(
+ "Unresolved constructor '{}', file: {}".format(param, state.current_class), state
+ )
+ ref_type = "_constructor"
if cmd.startswith("method"):
if method_param not in class_def.methods:
print_error("Unresolved method '{}', file: {}".format(param, state.current_class), state)
ref_type = "_method"
+ if cmd.startswith("operator"):
+ if method_param not in class_def.operators:
+ print_error("Unresolved operator '{}', file: {}".format(param, state.current_class), state)
+ ref_type = "_operator"
elif cmd.startswith("member"):
if method_param not in class_def.properties:
print_error("Unresolved member '{}', file: {}".format(param, state.current_class), state)
ref_type = "_property"
+ elif cmd.startswith("theme_item"):
+ if method_param not in class_def.theme_items:
+ print_error(
+ "Unresolved theme item '{}', file: {}".format(param, state.current_class), state
+ )
+ ref_type = "_theme_{}".format(class_def.theme_items[method_param].data_name)
+
elif cmd.startswith("signal"):
if method_param not in class_def.signals:
print_error("Unresolved signal '{}', file: {}".format(param, state.current_class), state)
@@ -814,17 +996,23 @@ def rstize_text(text, state): # type: (str, State) -> str
elif cmd.find("image=") == 0:
tag_text = "" # '![](' + cmd[6:] + ')'
elif cmd.find("url=") == 0:
- url_link = cmd[4:]
- tag_text = "`"
- tag_depth += 1
- inside_url = True
- url_has_name = False
- elif cmd == "/url":
- tag_text = ("" if url_has_name else url_link) + " <" + url_link + ">`_"
- tag_depth -= 1
- escape_post = True
- inside_url = False
- url_has_name = False
+ # URLs are handled in full here as we need to extract the optional link
+ # title to use `make_link`.
+ link_url = cmd[4:]
+ endurl_pos = text.find("[/url]", endq_pos + 1)
+ if endurl_pos == -1:
+ print_error(
+ "Tag depth mismatch for [url]: no closing [/url], file: {}".format(state.current_class), state
+ )
+ break
+ link_title = text[endq_pos + 1 : endurl_pos]
+ tag_text = make_link(link_url, link_title)
+
+ pre_text = text[:pos]
+ text = pre_text + tag_text + text[endurl_pos + 6 :]
+ pos = len(pre_text) + len(tag_text)
+ previous_pos = pos
+ continue
elif cmd == "center":
tag_depth += 1
tag_text = ""
@@ -837,7 +1025,7 @@ def rstize_text(text, state): # type: (str, State) -> str
inside_code = True
elif cmd == "gdscript":
tag_depth += 1
- tag_text = "\n .. code-tab:: gdscript GDScript\n"
+ tag_text = "\n .. code-tab:: gdscript\n"
inside_code = True
elif cmd == "csharp":
tag_depth += 1
@@ -956,6 +1144,8 @@ def format_table(f, data, remove_empty_columns=False): # type: (TextIO, Iterabl
def make_type(klass, state): # type: (str, State) -> str
+ if klass.find("*") != -1: # Pointer, ignore
+ return klass
link_type = klass
if link_type.endswith("[]"): # Typed array, strip [] to link to contained type.
link_type = link_type[:-2]
@@ -980,9 +1170,6 @@ def make_enum(t, state): # type: (str, State) -> str
if c in state.classes and e not in state.classes[c].enums:
c = "@GlobalScope"
- if not c in state.classes and c.startswith("_"):
- c = c[1:] # Remove the underscore prefix
-
if c in state.classes and e in state.classes[c].enums:
return ":ref:`{0}<enum_{1}_{0}>`".format(e, c)
@@ -994,19 +1181,26 @@ def make_enum(t, state): # type: (str, State) -> str
def make_method_signature(
- class_def, method_def, make_ref, state
-): # type: (ClassDef, Union[MethodDef, SignalDef], bool, State) -> Tuple[str, str]
+ class_def, method_def, ref_type, state
+): # type: (ClassDef, Union[MethodDef, SignalDef], str, State) -> Tuple[str, str]
ret_type = " "
- ref_type = "signal"
if isinstance(method_def, MethodDef):
ret_type = method_def.return_type.to_rst(state)
- ref_type = "method"
out = ""
- if make_ref:
- out += ":ref:`{0}<class_{1}_{2}_{0}>` ".format(method_def.name, class_def.name, ref_type)
+ if ref_type != "":
+ if ref_type == "operator":
+ out += ":ref:`{0}<class_{1}_{2}_{3}_{4}>` ".format(
+ method_def.name,
+ class_def.name,
+ ref_type,
+ sanitize_operator_name(method_def.name, state),
+ method_def.return_type.type_name,
+ )
+ else:
+ out += ":ref:`{0}<class_{1}_{2}_{0}>` ".format(method_def.name, class_def.name, ref_type)
else:
out += "**{}** ".format(method_def.name)
@@ -1052,30 +1246,91 @@ def make_footer(): # type: () -> str
".. |const| replace:: :abbr:`const (This method has no side effects. It doesn't modify any of the instance's member variables.)`\n"
".. |vararg| replace:: :abbr:`vararg (This method accepts any number of arguments after the ones described here.)`\n"
".. |constructor| replace:: :abbr:`constructor (This method is used to construct a type.)`\n"
+ ".. |static| replace:: :abbr:`static (This method doesn't need an instance to be called, so it can be called directly using the class name.)`\n"
".. |operator| replace:: :abbr:`operator (This method describes a valid operator to use with this type as left-hand operand.)`\n"
)
# fmt: on
-def make_url(link): # type: (str) -> str
- match = GODOT_DOCS_PATTERN.search(link)
+def make_link(url, title): # type: (str, str) -> str
+ match = GODOT_DOCS_PATTERN.search(url)
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] + "`"
+ # Or use the title if provided.
+ if title != "":
+ return "`" + title + " <../" + groups[0] + ".html" + groups[1] + ">`__"
+ return "`" + groups[1] + " <../" + groups[0] + ".html" + groups[1] + ">`__ in :doc:`../" + groups[0] + "`"
elif match.lastindex == 1:
# Doc reference, for example:
# `Math`
+ if title != "":
+ return ":doc:`" + title + " <../" + groups[0] + ">`"
return ":doc:`../" + groups[0] + "`"
else:
# External link, for example:
# `http://enet.bespin.org/usergroup0.html`
- return "`" + link + " <" + link + ">`_"
+ if title != "":
+ return "`" + title + " <" + url + ">`__"
+ return "`" + url + " <" + url + ">`__"
+
+
+def sanitize_operator_name(dirty_name, state): # type: (str, State) -> str
+ clear_name = dirty_name.replace("operator ", "")
+
+ if clear_name == "!=":
+ clear_name = "neq"
+ elif clear_name == "==":
+ clear_name = "eq"
+
+ elif clear_name == "<":
+ clear_name = "lt"
+ elif clear_name == "<=":
+ clear_name = "lte"
+ elif clear_name == ">":
+ clear_name = "gt"
+ elif clear_name == ">=":
+ clear_name = "gte"
+
+ elif clear_name == "+":
+ clear_name = "sum"
+ elif clear_name == "-":
+ clear_name = "dif"
+ elif clear_name == "*":
+ clear_name = "mul"
+ elif clear_name == "/":
+ clear_name = "div"
+ elif clear_name == "%":
+ clear_name = "mod"
+
+ elif clear_name == "unary+":
+ clear_name = "unplus"
+ elif clear_name == "unary-":
+ clear_name = "unminus"
+
+ elif clear_name == "<<":
+ clear_name = "bwsl"
+ elif clear_name == ">>":
+ clear_name = "bwsr"
+ elif clear_name == "&":
+ clear_name = "bwand"
+ elif clear_name == "|":
+ clear_name = "bwor"
+ elif clear_name == "^":
+ clear_name = "bwxor"
+ elif clear_name == "~":
+ clear_name = "bwnot"
+
+ elif clear_name == "[]":
+ clear_name = "idx"
+
+ else:
+ clear_name = "xxx"
+ print_error("Unsupported operator type '{}', please add the missing rule.".format(dirty_name), state)
+
+ return clear_name
if __name__ == "__main__":