diff options
Diffstat (limited to 'doc/tools/make_rst.py')
| -rwxr-xr-x | doc/tools/make_rst.py | 215 | 
1 files changed, 162 insertions, 53 deletions
| diff --git a/doc/tools/make_rst.py b/doc/tools/make_rst.py index fe8083f218..9e18866ca3 100755 --- a/doc/tools/make_rst.py +++ b/doc/tools/make_rst.py @@ -11,10 +11,49 @@ 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(#.*)?$") + +# 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 @@ -42,15 +81,15 @@ class TypeName:  class PropertyDef:      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, type_name, setter, getter, text, default_value, overrides +    ):  # type: (str, TypeName, Optional[str], Optional[str], Optional[str], Optional[str], Optional[str]) -> 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 +        self.overrides = overrides  class ParameterDef: @@ -164,10 +203,10 @@ class State:                  default_value = property.get("default") or None                  if default_value is not None:                      default_value = "``{}``".format(default_value) -                overridden = property.get("override") or False +                overrides = property.get("overrides") or None                  property_def = PropertyDef( -                    property_name, type_name, setter, getter, property.text, default_value, overridden +                    property_name, type_name, setter, getter, property.text, default_value, overrides                  )                  class_def.properties[property_name] = property_def @@ -330,7 +369,7 @@ class State:                      theme_item.text,                      default_value,                  ) -                class_def.theme_items[theme_item_id] = theme_item_def +                class_def.theme_items[theme_item_name] = theme_item_def          tutorials = class_root.find("tutorials")          if tutorials is not None: @@ -364,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( @@ -373,6 +413,25 @@ 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] @@ -449,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 @@ -464,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: @@ -493,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,8 +589,10 @@ 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 default is not None and property_def.overridden: -                ml.append((type_rst, property_def.name, default + " *(parent override)*")) +            if default is not None and property_def.overrides: +                ref = ":ref:`{1}<class_{1}_property_{0}>`".format(property_def.name, property_def.overrides) +                # 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)) @@ -626,12 +695,12 @@ def make_rst_class(class_def, state, dry_run, output_dir):  # type: (ClassDef, S              f.write("\n\n")      # Property descriptions -    if any(not p.overridden for p in class_def.properties.values()) > 0: +    if any(not p.overrides for p in class_def.properties.values()) > 0:          f.write(make_heading("Property Descriptions", "-"))          index = 0          for property_def in class_def.properties.values(): -            if property_def.overridden: +            if property_def.overrides:                  continue              if index != 0: @@ -641,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) @@ -735,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) @@ -845,28 +916,23 @@ 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)      # 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 @@ -878,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: @@ -886,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 @@ -912,6 +980,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 :] @@ -948,6 +1017,13 @@ def rstize_text(text, state):  # type: (str, State) -> str                              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) @@ -991,21 +1067,35 @@ 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 = ""  # ''              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] +                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              elif cmd == "center":                  tag_depth += 1                  tag_text = "" @@ -1039,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) @@ -1226,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" @@ -1235,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 @@ -1252,21 +1360,22 @@ def make_link(url, title):  # type: (str, str) -> str          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`          if title != "":              return "`" + title + " <" + url + ">`__" -        else: -            return "`" + url + " <" + url + ">`__" +        return "`" + url + " <" + url + ">`__"  def sanitize_operator_name(dirty_name, state):  # type: (str, State) -> str |