diff options
Diffstat (limited to 'doc/tools/make_rst.py')
-rwxr-xr-x | doc/tools/make_rst.py | 140 |
1 files changed, 120 insertions, 20 deletions
diff --git a/doc/tools/make_rst.py b/doc/tools/make_rst.py index 1523194a85..68f3b66f43 100755 --- a/doc/tools/make_rst.py +++ b/doc/tools/make_rst.py @@ -14,6 +14,47 @@ from collections import OrderedDict # $DOCS_URL/path/to/page.html(#fragment-tag) GODOT_DOCS_PATTERN = re.compile(r"^\$DOCS_URL/(.*)\.html(#.*)?$") +# Based on reStructedText inline markup recognition rules +# https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#inline-markup-recognition-rules +MARKUP_ALLOWED_PRECEDENT = " -:/'\"<([{" +MARKUP_ALLOWED_SUBSEQUENT = " -.,:;!?\\/'\")]}>" + +# Used to translate section headings and other hardcoded strings when required with +# the --lang argument. The BASE_STRINGS list should be synced with what we actually +# write in this script (check `translate()` uses), and also hardcoded in +# `doc/translations/extract.py` to include them in the source POT file. +BASE_STRINGS = [ + "Description", + "Tutorials", + "Properties", + "Constructors", + "Methods", + "Operators", + "Theme Properties", + "Signals", + "Enumerations", + "Constants", + "Property Descriptions", + "Constructor Descriptions", + "Method Descriptions", + "Operator Descriptions", + "Theme Property Descriptions", + "Inherits:", + "Inherited By:", + "(overrides %s)", + "Default", + "Setter", + "value", + "Getter", + "This method should typically be overridden by the user to have any effect.", + "This method has no side effects. It doesn't modify any of the instance's member variables.", + "This method accepts any number of arguments after the ones described here.", + "This method is used to construct a type.", + "This method doesn't need an instance to be called, so it can be called directly using the class name.", + "This method describes a valid operator to use with this type as left-hand operand.", +] +strings_l10n = {} + def print_error(error, state): # type: (str, State) -> None print("ERROR: {}".format(error)) @@ -362,6 +403,7 @@ def main(): # type: () -> None parser = argparse.ArgumentParser() parser.add_argument("path", nargs="+", help="A path to an XML file or a directory containing XML files to parse.") parser.add_argument("--filter", default="", help="The filepath pattern for XML files to filter.") + parser.add_argument("--lang", "-l", default="en", help="Language to use for section headings.") group = parser.add_mutually_exclusive_group() group.add_argument("--output", "-o", default=".", help="The directory to save output .rst files in.") group.add_argument( @@ -371,13 +413,32 @@ def main(): # type: () -> None ) args = parser.parse_args() + # Retrieve heading translations for the given language. + if not args.dry_run and args.lang != "en": + lang_file = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "..", "translations", "{}.po".format(args.lang) + ) + if os.path.exists(lang_file): + try: + import polib + except ImportError: + print("Base template strings localization requires `polib`.") + exit(1) + + pofile = polib.pofile(lang_file) + for entry in pofile.translated_entries(): + if entry.msgid in BASE_STRINGS: + strings_l10n[entry.msgid] = entry.msgstr + else: + print("No PO file at '{}' for language '{}'.".format(lang_file, args.lang)) + print("Checking for errors in the XML class reference...") file_list = [] # type: List[str] for path in args.path: # Cut off trailing slashes so os.path.basename doesn't choke. - if path.endswith(os.sep): + if path.endswith("/") or path.endswith("\\"): path = path[:-1] if os.path.basename(path) == "modules": @@ -447,6 +508,14 @@ def main(): # type: () -> None exit(1) +def translate(string): # type: (str) -> str + """Translate a string based on translations sourced from `doc/translations/*.po` + for a language if defined via the --lang command line argument. + Returns the original string if no translation exists. + """ + return strings_l10n.get(string, string) + + def make_rst_class(class_def, state, dry_run, output_dir): # type: (ClassDef, State, bool, str) -> None class_name = class_def.name @@ -462,13 +531,13 @@ def make_rst_class(class_def, state, dry_run, output_dir): # type: (ClassDef, S f.write(".. The source is found in doc/classes or modules/<name>/doc_classes.\n\n") f.write(".. _class_" + class_name + ":\n\n") - f.write(make_heading(class_name, "=")) + f.write(make_heading(class_name, "=", False)) # Inheritance tree # Ascendants if class_def.inherits: inh = class_def.inherits.strip() - f.write("**Inherits:** ") + f.write("**" + translate("Inherits:") + "** ") first = True while inh in state.classes: if not first: @@ -491,7 +560,7 @@ def make_rst_class(class_def, state, dry_run, output_dir): # type: (ClassDef, S inherited.append(c.name) if len(inherited): - f.write("**Inherited By:** ") + f.write("**" + translate("Inherited By:") + "** ") for i, child in enumerate(inherited): if i > 0: f.write(", ") @@ -522,7 +591,8 @@ def make_rst_class(class_def, state, dry_run, output_dir): # type: (ClassDef, S default = property_def.default_value if default is not None and property_def.overrides: ref = ":ref:`{1}<class_{1}_property_{0}>`".format(property_def.name, property_def.overrides) - ml.append((type_rst, property_def.name, default + " (overrides " + ref + ")")) + # Not using translate() for now as it breaks table formatting. + ml.append((type_rst, property_def.name, default + " " + "(overrides %s)" % ref)) else: ref = ":ref:`{0}<class_{1}_property_{0}>`".format(property_def.name, class_name) ml.append((type_rst, ref, default)) @@ -640,12 +710,13 @@ def make_rst_class(class_def, state, dry_run, output_dir): # type: (ClassDef, S f.write("- {} **{}**\n\n".format(property_def.type_name.to_rst(state), property_def.name)) info = [] + # Not using translate() for now as it breaks table formatting. if property_def.default_value is not None: - info.append(("*Default*", property_def.default_value)) + info.append(("*" + "Default" + "*", property_def.default_value)) if property_def.setter is not None and not property_def.setter.startswith("_"): - info.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("_"): - info.append(("*Getter*", property_def.getter + "()")) + info.append(("*" + "Getter" + "*", property_def.getter + "()")) if len(info) > 0: format_table(f, info) @@ -734,7 +805,8 @@ def make_rst_class(class_def, state, dry_run, output_dir): # type: (ClassDef, S info = [] if theme_item_def.default_value is not None: - info.append(("*Default*", theme_item_def.default_value)) + # Not using translate() for now as it breaks table formatting. + info.append(("*" + "Default" + "*", theme_item_def.default_value)) if len(info) > 0: format_table(f, info) @@ -844,12 +916,12 @@ def rstize_text(text, state): # type: (str, State) -> str if result is None: return "" text = pre_text + result[0] - pos += result[1] + pos += result[1] - indent_level # Handle normal text else: text = pre_text + "\n\n" + post_text - pos += 2 + pos += 2 - indent_level next_brac_pos = text.find("[") text = escape_rst(text, next_brac_pos) @@ -872,6 +944,7 @@ def rstize_text(text, state): # type: (str, State) -> str post_text = text[endq_pos + 1 :] tag_text = text[pos + 1 : endq_pos] + escape_pre = False escape_post = False if tag_text in state.classes: @@ -880,6 +953,7 @@ def rstize_text(text, state): # type: (str, State) -> str tag_text = "``{}``".format(tag_text) else: tag_text = make_type(tag_text, state) + escape_pre = True escape_post = True else: # command cmd = tag_text @@ -993,6 +1067,7 @@ def rstize_text(text, state): # type: (str, State) -> str if class_param != state.current_class: repl_text = "{}.{}".format(class_param, method_param) tag_text = ":ref:`{}<class_{}{}_{}>`".format(repl_text, class_param, ref_type, method_param) + escape_pre = True escape_post = True elif cmd.find("image=") == 0: tag_text = "" # '' @@ -1010,7 +1085,14 @@ def rstize_text(text, state): # type: (str, State) -> str tag_text = make_link(link_url, link_title) pre_text = text[:pos] - text = pre_text + tag_text + text[endurl_pos + 6 :] + post_text = text[endurl_pos + 6 :] + + if pre_text and pre_text[-1] not in MARKUP_ALLOWED_PRECEDENT: + pre_text += "\ " + if post_text and post_text[0] not in MARKUP_ALLOWED_SUBSEQUENT: + post_text = "\ " + post_text + + text = pre_text + tag_text + post_text pos = len(pre_text) + len(tag_text) previous_pos = pos continue @@ -1047,40 +1129,53 @@ def rstize_text(text, state): # type: (str, State) -> str elif cmd == "i" or cmd == "/i": if cmd == "/i": tag_depth -= 1 + escape_post = True else: tag_depth += 1 + escape_pre = True tag_text = "*" elif cmd == "b" or cmd == "/b": if cmd == "/b": tag_depth -= 1 + escape_post = True else: tag_depth += 1 + escape_pre = True tag_text = "**" elif cmd == "u" or cmd == "/u": if cmd == "/u": tag_depth -= 1 + escape_post = True else: tag_depth += 1 + escape_pre = True tag_text = "" elif cmd == "code": tag_text = "``" tag_depth += 1 inside_code = True + escape_pre = True elif cmd == "kbd": tag_text = ":kbd:`" tag_depth += 1 + escape_pre = True elif cmd == "/kbd": tag_text = "`" tag_depth -= 1 + escape_post = True elif cmd.startswith("enum "): tag_text = make_enum(cmd[5:], state) + escape_pre = True escape_post = True else: tag_text = make_type(tag_text, state) + escape_pre = True escape_post = True # Properly escape things like `[Node]s` - if escape_post and post_text and (post_text[0].isalnum() or post_text[0] == "("): # not punctuation, escape + if escape_pre and pre_text and pre_text[-1] not in MARKUP_ALLOWED_PRECEDENT: + pre_text += "\ " + if escape_post and post_text and post_text[0] not in MARKUP_ALLOWED_SUBSEQUENT: post_text = "\ " + post_text next_brac_pos = post_text.find("[", 0) @@ -1234,7 +1329,12 @@ def make_method_signature( return ret_type, out -def make_heading(title, underline): # type: (str, str) -> str +def make_heading(title, underline, l10n=True): # type: (str, str, bool) -> str + if l10n: + new_title = translate(title) + if new_title != title: + title = new_title + underline *= 2 # Double length to handle wide chars. return title + "\n" + (underline * len(title)) + "\n\n" @@ -1243,12 +1343,12 @@ def make_footer(): # type: () -> str # This way, we avoid bloating the generated rST with duplicate abbreviations. # fmt: off return ( - ".. |virtual| replace:: :abbr:`virtual (This method should typically be overridden by the user to have any effect.)`\n" - ".. |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" + ".. |virtual| replace:: :abbr:`virtual (" + translate("This method should typically be overridden by the user to have any effect.") + ")`\n" + ".. |const| replace:: :abbr:`const (" + translate("This method has no side effects. It doesn't modify any of the instance's member variables.") + ")`\n" + ".. |vararg| replace:: :abbr:`vararg (" + translate("This method accepts any number of arguments after the ones described here.") + ")`\n" + ".. |constructor| replace:: :abbr:`constructor (" + translate("This method is used to construct a type.") + ")`\n" + ".. |static| replace:: :abbr:`static (" + translate("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 (" + translate("This method describes a valid operator to use with this type as left-hand operand.") + ")`\n" ) # fmt: on |