diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml
new file mode 100644
index 000000000..81e0564fa
--- /dev/null
+++ b/.github/workflows/docs.yaml
@@ -0,0 +1,28 @@
+name: "π Documentation"
+on:
+ push:
+ branches:
+ - main
+permissions:
+ contents: write
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Configure Git Credentials
+ run: |
+ git config user.name github-actions[bot]
+ git config user.email 41898282+github-actions[bot]@users.noreply.github.com
+ - uses: actions/setup-python@v5
+ with:
+ python-version: 3.x
+ - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
+ - uses: actions/cache@v4
+ with:
+ key: mkdocs-material-${{ env.cache_id }}
+ path: ~/.cache
+ restore-keys: |
+ mkdocs-material-
+ - run: pip install mkdocs-material mkdocs-awesome-nav
+ - run: mkdocs gh-deploy --force
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 0494b2cda..10d9c1a59 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -14,4 +14,4 @@ jobs:
steps:
- uses: actions/checkout@v3
- run: |
- make in-docker IMAGE_TAG=4.4.1 TARGET='import test GAMESCOPE_CMD='
+ make in-docker IMAGE_TAG=4.5.1 TARGET='import docs dist test GAMESCOPE_CMD='
diff --git a/.gitignore b/.gitignore
index 517c866f1..4d6ed0ef9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@ settings.mk
RyzenAdj
.gut_editor_config.json
result
+target
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 000000000..4adb33d48
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,18 @@
+# .readthedocs.yaml
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+# Required
+version: 2
+
+# Set the OS, Python version and other tools you might need
+build:
+ os: ubuntu-24.04
+ tools:
+ python: "3"
+ jobs:
+ pre_install:
+ - pip install mkdocs-material mkdocs-awesome-nav
+
+mkdocs:
+ configuration: mkdocs.yml
diff --git a/.releaserc.yaml b/.releaserc.yaml
index 00f86b5ab..76c60c692 100644
--- a/.releaserc.yaml
+++ b/.releaserc.yaml
@@ -35,7 +35,7 @@ plugins:
# Execute commands to build the project
- - "@semantic-release/exec"
- shell: true
- prepareCmd: "make in-docker IMAGE_TAG=4.5.1 TARGET='force-import dist update-pkgbuild-hash'"
+ prepareCmd: "make in-docker IMAGE_TAG=4.5.1 TARGET='force-import docs dist update-pkgbuild-hash'"
publishCmd: "echo '${nextRelease.version}' > .version.txt"
# Commit the following changes to git after other plugins have run
@@ -44,6 +44,8 @@ plugins:
- core/global/version.tres
- package/rpm/opengamepadui.spec
- package/archlinux/PKGBUILD
+ - docs/class-reference/.nav.yml
+ - docs/class-reference/*.md
# Publish artifacts as a GitHub release
- - "@semantic-release/github"
diff --git a/Makefile b/Makefile
index 7c419f5c1..1ac624295 100644
--- a/Makefile
+++ b/Makefile
@@ -207,25 +207,38 @@ debug-overlay: $(IMPORT_DIR) ## Run the project in debug mode in gamescope with
--position 320,140 res://entrypoint.tscn --overlay-mode -- steam -gamepadui -steamos3 -steampal -steamdeck
.PHONY: docs
-docs: docs/api/classes/.generated ## Generate docs
+docs: docs/api/classes/.generated ## Generate class reference docs
docs/api/classes/.generated: $(IMPORT_DIR) $(ALL_GDSCRIPT)
- rm -rf docs/api/classes
- mkdir -p docs/api/classes
+ rm -rf $(CACHE_DIR)/docs/api/classes
+ mkdir -p $(CACHE_DIR)/docs/api/classes
+ @echo "Generating GDExtension class references"
$(GODOT) \
--editor \
--quit \
- --doctool docs/api/classes \
+ --doctool $(CACHE_DIR)/docs/api/classes \
--no-docbase \
--gdextension-docs
+ mv $(CACHE_DIR)/docs/api/classes/doc_classes/*.xml $(CACHE_DIR)/docs/api/classes
+ rmdir $(CACHE_DIR)/docs/api/classes/doc_classes
+ @echo "Generating GDScript class references"
$(GODOT) \
--editor \
--path $(PWD) \
--quit \
- --doctool docs/api/classes \
+ --doctool $(CACHE_DIR)/docs/api/classes \
--no-docbase \
- --gdscript-docs core
- rm -rf docs/api/classes/core--*
- $(MAKE) -C docs/api rst
+ --gdscript-docs .
+ @echo "Removing non-class documentation"
+ rm $(CACHE_DIR)/docs/api/classes/entrypoint.gd.xml
+ rm -rf $(CACHE_DIR)/docs/api/classes/core--*
+ rm -rf $(CACHE_DIR)/docs/api/classes/addons--*
+ rm -rf $(CACHE_DIR)/docs/api/classes/plugins--*
+ $(MAKE) docgen
+
+docgen:
+ rm -f ./docs/class-reference/*.md
+ cd ./extensions && \
+ cargo run --bin docgen -- ../$(CACHE_DIR)/docs/api/classes ../docs/class-reference
.PHONY: inspect
inspect: $(IMPORT_DIR) ## Launch Gamescope inspector
diff --git a/README.md b/README.md
index 3c7c413d2..a02eb8588 100644
--- a/README.md
+++ b/README.md
@@ -11,11 +11,11 @@
-
+
-
-
-
+
+
+
diff --git a/core/systems/threading/linuxthread_test.gd b/core/systems/threading/linuxthread_test.gd
deleted file mode 100644
index 97396e3f2..000000000
--- a/core/systems/threading/linuxthread_test.gd
+++ /dev/null
@@ -1,10 +0,0 @@
-extends GutTest
-
-
-func test_subreaper_create_process() -> void:
- var pid := SubReaper.create_process("sleep", ["1"])
- gut.p("Got reaper PID: " + str(pid))
-
- assert_true(OS.is_process_running(pid), "reaper process should be running")
- await wait_seconds(2)
- assert_false(OS.is_process_running(pid), "reaper process should have exited")
diff --git a/docs/.nav.yml b/docs/.nav.yml
new file mode 100644
index 000000000..193f56c62
--- /dev/null
+++ b/docs/.nav.yml
@@ -0,0 +1,4 @@
+nav:
+ - Home: ./index.md
+ - ./documentation
+ - ./class-reference
diff --git a/docs/api/.gitignore b/docs/api/.gitignore
deleted file mode 100644
index 51b6dc595..000000000
--- a/docs/api/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-_build
-classes
diff --git a/docs/api/Makefile b/docs/api/Makefile
deleted file mode 100644
index 495affdbd..000000000
--- a/docs/api/Makefile
+++ /dev/null
@@ -1,21 +0,0 @@
-BASEDIR = .
-CLASSES = "$(BASEDIR)/classes/"
-OUTPUTDIR = $(BASEDIR)/_build
-TOOLSDIR = $(BASEDIR)/tools
-LANGARG ?= en
-LANGCMD = -l $(LANGARG)
-
-.ONESHELL:
-
-clean:
- rm -rf "$(OUTPUTDIR)"
-
-doxygen:
- rm -rf "$(OUTPUTDIR)/doxygen"
- mkdir -p "$(OUTPUTDIR)/doxygen"
- doxygen Doxyfile
-
-rst:
- rm -rf "$(OUTPUTDIR)/rst"
- mkdir -p "$(OUTPUTDIR)/rst"
- python3 "$(TOOLSDIR)/make_rst.py" -o "$(OUTPUTDIR)/rst" "$(LANGCMD)" $(CLASSES)
diff --git a/docs/api/tools/.gitignore b/docs/api/tools/.gitignore
deleted file mode 100644
index bee8a64b7..000000000
--- a/docs/api/tools/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-__pycache__
diff --git a/docs/api/tools/doc_status.py b/docs/api/tools/doc_status.py
deleted file mode 100755
index 717a468b3..000000000
--- a/docs/api/tools/doc_status.py
+++ /dev/null
@@ -1,502 +0,0 @@
-#!/usr/bin/env python3
-
-import fnmatch
-import os
-import sys
-import re
-import math
-import platform
-import xml.etree.ElementTree as ET
-from typing import Dict, List, Set
-
-################################################################################
-# Config #
-################################################################################
-
-flags = {
- "c": platform.platform() != "Windows", # Disable by default on windows, since we use ANSI escape codes
- "b": False,
- "g": False,
- "s": False,
- "u": False,
- "h": False,
- "p": False,
- "o": True,
- "i": False,
- "a": True,
- "e": False,
-}
-flag_descriptions = {
- "c": "Toggle colors when outputting.",
- "b": "Toggle showing only not fully described classes.",
- "g": "Toggle showing only completed classes.",
- "s": "Toggle showing comments about the status.",
- "u": "Toggle URLs to docs.",
- "h": "Show help and exit.",
- "p": "Toggle showing percentage as well as counts.",
- "o": "Toggle overall column.",
- "i": "Toggle collapse of class items columns.",
- "a": "Toggle showing all items.",
- "e": "Toggle hiding empty items.",
-}
-long_flags = {
- "colors": "c",
- "use-colors": "c",
- "bad": "b",
- "only-bad": "b",
- "good": "g",
- "only-good": "g",
- "comments": "s",
- "status": "s",
- "urls": "u",
- "gen-url": "u",
- "help": "h",
- "percent": "p",
- "use-percentages": "p",
- "overall": "o",
- "use-overall": "o",
- "items": "i",
- "collapse": "i",
- "all": "a",
- "empty": "e",
-}
-table_columns = [
- "name",
- "brief_description",
- "description",
- "methods",
- "constants",
- "members",
- "theme_items",
- "signals",
- "operators",
- "constructors",
-]
-table_column_names = [
- "Name",
- "Brief Desc.",
- "Desc.",
- "Methods",
- "Constants",
- "Members",
- "Theme Items",
- "Signals",
- "Operators",
- "Constructors",
-]
-colors = {
- "name": [36], # cyan
- "part_big_problem": [4, 31], # underline, red
- "part_problem": [31], # red
- "part_mostly_good": [33], # yellow
- "part_good": [32], # green
- "url": [4, 34], # underline, blue
- "section": [1, 4], # bold, underline
- "state_off": [36], # cyan
- "state_on": [1, 35], # bold, magenta/plum
- "bold": [1], # bold
-}
-overall_progress_description_weight = 10
-
-
-################################################################################
-# Utils #
-################################################################################
-
-
-def validate_tag(elem: ET.Element, tag: str) -> None:
- if elem.tag != tag:
- print('Tag mismatch, expected "' + tag + '", got ' + elem.tag)
- sys.exit(255)
-
-
-def color(color: str, string: str) -> str:
- if flags["c"] and terminal_supports_color():
- color_format = ""
- for code in colors[color]:
- color_format += "\033[" + str(code) + "m"
- return color_format + string + "\033[0m"
- else:
- return string
-
-
-ansi_escape = re.compile(r"\x1b[^m]*m")
-
-
-def nonescape_len(s: str) -> int:
- return len(ansi_escape.sub("", s))
-
-
-def terminal_supports_color():
- p = sys.platform
- supported_platform = p != "Pocket PC" and (p != "win32" or "ANSICON" in os.environ)
-
- is_a_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
- if not supported_platform or not is_a_tty:
- return False
- return True
-
-
-################################################################################
-# Classes #
-################################################################################
-
-
-class ClassStatusProgress:
- def __init__(self, described: int = 0, total: int = 0):
- self.described: int = described
- self.total: int = total
-
- def __add__(self, other: "ClassStatusProgress"):
- return ClassStatusProgress(self.described + other.described, self.total + other.total)
-
- def increment(self, described: bool):
- if described:
- self.described += 1
- self.total += 1
-
- def is_ok(self):
- return self.described >= self.total
-
- def to_configured_colored_string(self):
- if flags["p"]:
- return self.to_colored_string("{percent}% ({has}/{total})", "{pad_percent}{pad_described}{s}{pad_total}")
- else:
- return self.to_colored_string()
-
- def to_colored_string(self, format: str = "{has}/{total}", pad_format: str = "{pad_described}{s}{pad_total}"):
- ratio = float(self.described) / float(self.total) if self.total != 0 else 1
- percent = int(round(100 * ratio))
- s = format.format(has=str(self.described), total=str(self.total), percent=str(percent))
- if self.described >= self.total:
- s = color("part_good", s)
- elif self.described >= self.total / 4 * 3:
- s = color("part_mostly_good", s)
- elif self.described > 0:
- s = color("part_problem", s)
- else:
- s = color("part_big_problem", s)
- pad_size = max(len(str(self.described)), len(str(self.total)))
- pad_described = "".ljust(pad_size - len(str(self.described)))
- pad_percent = "".ljust(3 - len(str(percent)))
- pad_total = "".ljust(pad_size - len(str(self.total)))
- return pad_format.format(pad_described=pad_described, pad_total=pad_total, pad_percent=pad_percent, s=s)
-
-
-class ClassStatus:
- def __init__(self, name: str = ""):
- self.name: str = name
- self.has_brief_description: bool = True
- self.has_description: bool = True
- self.progresses: Dict[str, ClassStatusProgress] = {
- "methods": ClassStatusProgress(),
- "constants": ClassStatusProgress(),
- "members": ClassStatusProgress(),
- "theme_items": ClassStatusProgress(),
- "signals": ClassStatusProgress(),
- "operators": ClassStatusProgress(),
- "constructors": ClassStatusProgress(),
- }
-
- def __add__(self, other: "ClassStatus"):
- new_status = ClassStatus()
- new_status.name = self.name
- new_status.has_brief_description = self.has_brief_description and other.has_brief_description
- new_status.has_description = self.has_description and other.has_description
- for k in self.progresses:
- new_status.progresses[k] = self.progresses[k] + other.progresses[k]
- return new_status
-
- def is_ok(self):
- ok = True
- ok = ok and self.has_brief_description
- ok = ok and self.has_description
- for k in self.progresses:
- ok = ok and self.progresses[k].is_ok()
- return ok
-
- def is_empty(self):
- sum = 0
- for k in self.progresses:
- if self.progresses[k].is_ok():
- continue
- sum += self.progresses[k].total
- return sum < 1
-
- def make_output(self) -> Dict[str, str]:
- output: Dict[str, str] = {}
- output["name"] = color("name", self.name)
-
- ok_string = color("part_good", "OK")
- missing_string = color("part_big_problem", "MISSING")
-
- output["brief_description"] = ok_string if self.has_brief_description else missing_string
- output["description"] = ok_string if self.has_description else missing_string
-
- description_progress = ClassStatusProgress(
- (self.has_brief_description + self.has_description) * overall_progress_description_weight,
- 2 * overall_progress_description_weight,
- )
- items_progress = ClassStatusProgress()
-
- for k in ["methods", "constants", "members", "theme_items", "signals", "constructors", "operators"]:
- items_progress += self.progresses[k]
- output[k] = self.progresses[k].to_configured_colored_string()
-
- output["items"] = items_progress.to_configured_colored_string()
-
- output["overall"] = (description_progress + items_progress).to_colored_string(
- color("bold", "{percent}%"), "{pad_percent}{s}"
- )
-
- if self.name.startswith("Total"):
- output["url"] = color("url", "https://docs.godotengine.org/en/latest/classes/")
- if flags["s"]:
- output["comment"] = color("part_good", "ALL OK")
- else:
- output["url"] = color(
- "url", "https://docs.godotengine.org/en/latest/classes/class_{name}.html".format(name=self.name.lower())
- )
-
- if flags["s"] and not flags["g"] and self.is_ok():
- output["comment"] = color("part_good", "ALL OK")
-
- return output
-
- @staticmethod
- def generate_for_class(c: ET.Element):
- status = ClassStatus()
- status.name = c.attrib["name"]
-
- for tag in list(c):
- len_tag_text = 0 if (tag.text is None) else len(tag.text.strip())
-
- if tag.tag == "brief_description":
- status.has_brief_description = len_tag_text > 0
-
- elif tag.tag == "description":
- status.has_description = len_tag_text > 0
-
- elif tag.tag in ["methods", "signals", "operators", "constructors"]:
- for sub_tag in list(tag):
- descr = sub_tag.find("description")
- increment = (descr is not None) and (descr.text is not None) and len(descr.text.strip()) > 0
- status.progresses[tag.tag].increment(increment)
- elif tag.tag in ["constants", "members", "theme_items"]:
- for sub_tag in list(tag):
- if not sub_tag.text is None:
- status.progresses[tag.tag].increment(len(sub_tag.text.strip()) > 0)
-
- elif tag.tag in ["tutorials"]:
- pass # Ignore those tags for now
-
- else:
- print(tag.tag, tag.attrib)
-
- return status
-
-
-################################################################################
-# Arguments #
-################################################################################
-
-input_file_list: List[str] = []
-input_class_list: List[str] = []
-merged_file: str = ""
-
-for arg in sys.argv[1:]:
- try:
- if arg.startswith("--"):
- flags[long_flags[arg[2:]]] = not flags[long_flags[arg[2:]]]
- elif arg.startswith("-"):
- for f in arg[1:]:
- flags[f] = not flags[f]
- elif os.path.isdir(arg):
- for f in os.listdir(arg):
- if f.endswith(".xml"):
- input_file_list.append(os.path.join(arg, f))
- else:
- input_class_list.append(arg)
- except KeyError:
- print("Unknown command line flag: " + arg)
- sys.exit(1)
-
-if flags["i"]:
- for r in ["methods", "constants", "members", "signals", "theme_items"]:
- index = table_columns.index(r)
- del table_column_names[index]
- del table_columns[index]
- table_column_names.append("Items")
- table_columns.append("items")
-
-if flags["o"] == (not flags["i"]):
- table_column_names.append(color("bold", "Overall"))
- table_columns.append("overall")
-
-if flags["u"]:
- table_column_names.append("Docs URL")
- table_columns.append("url")
-
-
-################################################################################
-# Help #
-################################################################################
-
-if len(input_file_list) < 1 or flags["h"]:
- if not flags["h"]:
- print(color("section", "Invalid usage") + ": Please specify a classes directory")
- print(color("section", "Usage") + ": doc_status.py [flags] [class names]")
- print("\t< and > signify required parameters, while [ and ] signify optional parameters.")
- print(color("section", "Available flags") + ":")
- possible_synonym_list = list(long_flags)
- possible_synonym_list.sort()
- flag_list = list(flags)
- flag_list.sort()
- for flag in flag_list:
- synonyms = [color("name", "-" + flag)]
- for synonym in possible_synonym_list:
- if long_flags[synonym] == flag:
- synonyms.append(color("name", "--" + synonym))
-
- print(
- (
- "{synonyms} (Currently "
- + color("state_" + ("on" if flags[flag] else "off"), "{value}")
- + ")\n\t{description}"
- ).format(
- synonyms=", ".join(synonyms),
- value=("on" if flags[flag] else "off"),
- description=flag_descriptions[flag],
- )
- )
- sys.exit(0)
-
-
-################################################################################
-# Parse class list #
-################################################################################
-
-class_names: List[str] = []
-classes: Dict[str, ET.Element] = {}
-
-for file in input_file_list:
- tree = ET.parse(file)
- doc = tree.getroot()
-
- if doc.attrib["name"] in class_names:
- continue
- class_names.append(doc.attrib["name"])
- classes[doc.attrib["name"]] = doc
-
-class_names.sort()
-
-if len(input_class_list) < 1:
- input_class_list = ["*"]
-
-filtered_classes_set: Set[str] = set()
-for pattern in input_class_list:
- filtered_classes_set |= set(fnmatch.filter(class_names, pattern))
-filtered_classes = list(filtered_classes_set)
-filtered_classes.sort()
-
-################################################################################
-# Make output table #
-################################################################################
-
-table = [table_column_names]
-table_row_chars = "| - "
-table_column_chars = "|"
-
-total_status = ClassStatus("Total")
-
-for cn in filtered_classes:
- c = classes[cn]
- validate_tag(c, "class")
- status = ClassStatus.generate_for_class(c)
-
- total_status = total_status + status
-
- if (flags["b"] and status.is_ok()) or (flags["g"] and not status.is_ok()) or (not flags["a"]):
- continue
-
- if flags["e"] and status.is_empty():
- continue
-
- out = status.make_output()
- row: List[str] = []
- for column in table_columns:
- if column in out:
- row.append(out[column])
- else:
- row.append("")
-
- if "comment" in out and out["comment"] != "":
- row.append(out["comment"])
-
- table.append(row)
-
-
-################################################################################
-# Print output table #
-################################################################################
-
-if len(table) == 1 and flags["a"]:
- print(color("part_big_problem", "No classes suitable for printing!"))
- sys.exit(0)
-
-if len(table) > 2 or not flags["a"]:
- total_status.name = "Total = {0}".format(len(table) - 1)
- out = total_status.make_output()
- row = []
- for column in table_columns:
- if column in out:
- row.append(out[column])
- else:
- row.append("")
- table.append(row)
-
-if flags["a"]:
- # Duplicate the headers at the bottom of the table so they can be viewed
- # without having to scroll back to the top.
- table.append(table_column_names)
-
-table_column_sizes: List[int] = []
-for row in table:
- for cell_i, cell in enumerate(row):
- if cell_i >= len(table_column_sizes):
- table_column_sizes.append(0)
-
- table_column_sizes[cell_i] = max(nonescape_len(cell), table_column_sizes[cell_i])
-
-divider_string = table_row_chars[0]
-for cell_i in range(len(table[0])):
- divider_string += (
- table_row_chars[1] + table_row_chars[2] * (table_column_sizes[cell_i]) + table_row_chars[1] + table_row_chars[0]
- )
-
-for row_i, row in enumerate(table):
- row_string = table_column_chars
- for cell_i, cell in enumerate(row):
- padding_needed = table_column_sizes[cell_i] - nonescape_len(cell) + 2
- if cell_i == 0:
- row_string += table_row_chars[3] + cell + table_row_chars[3] * (padding_needed - 1)
- else:
- row_string += (
- table_row_chars[3] * int(math.floor(float(padding_needed) / 2))
- + cell
- + table_row_chars[3] * int(math.ceil(float(padding_needed) / 2))
- )
- row_string += table_column_chars
-
- print(row_string)
-
- # Account for the possible double header (if the `a` flag is enabled).
- # No need to have a condition for the flag, as this will behave correctly
- # if the flag is disabled.
- if row_i == 0 or row_i == len(table) - 3 or row_i == len(table) - 2:
- print(divider_string)
-
-print(divider_string)
-
-if total_status.is_ok() and not flags["g"]:
- print("All listed classes are " + color("part_good", "OK") + "!")
diff --git a/docs/api/tools/make_rst.py b/docs/api/tools/make_rst.py
deleted file mode 100755
index 3c65d2f5b..000000000
--- a/docs/api/tools/make_rst.py
+++ /dev/null
@@ -1,2264 +0,0 @@
-#!/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 platform
-import re
-import sys
-import xml.etree.ElementTree as ET
-from collections import OrderedDict
-from typing import List, Dict, TextIO, Tuple, Optional, Any, Union
-
-# Import hardcoded version information from version.py
-root_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../")
-sys.path.append(root_directory) # Include the root directory
-import version
-
-# $DOCS_URL/path/to/page.html(#fragment-tag)
-GODOT_DOCS_PATTERN = re.compile(r"^\$DOCS_URL/(.*)\.html(#.*)?$")
-
-# Based on reStructuredText 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
-# `scripts/extract_classes.py` (godotengine/godot-editor-l10n repo) to include them in the source POT file.
-BASE_STRINGS = [
- "All classes",
- "Globals",
- "Nodes",
- "Resources",
- "Editor-only",
- "Other objects",
- "Variant types",
- "Description",
- "Tutorials",
- "Properties",
- "Constructors",
- "Methods",
- "Operators",
- "Theme Properties",
- "Signals",
- "Enumerations",
- "Constants",
- "Annotations",
- "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.",
- "This value is an integer composed as a bitmask of the following flags.",
- "There is currently no description for this class. Please help us by :ref:`contributing one `!",
- "There is currently no description for this signal. Please help us by :ref:`contributing one `!",
- "There is currently no description for this annotation. Please help us by :ref:`contributing one `!",
- "There is currently no description for this property. Please help us by :ref:`contributing one `!",
- "There is currently no description for this constructor. Please help us by :ref:`contributing one `!",
- "There is currently no description for this method. Please help us by :ref:`contributing one `!",
- "There is currently no description for this operator. Please help us by :ref:`contributing one `!",
- "There is currently no description for this theme property. Please help us by :ref:`contributing one `!",
- "There are notable differences when using this API with C#. See :ref:`doc_c_sharp_differences` for more information.",
-]
-strings_l10n: Dict[str, str] = {}
-
-STYLES: Dict[str, str] = {}
-
-CLASS_GROUPS: Dict[str, str] = {
- "global": "Globals",
- "node": "Nodes",
- "resource": "Resources",
- "object": "Other objects",
- "editor": "Editor-only",
- "variant": "Variant types",
-}
-CLASS_GROUPS_BASE: Dict[str, str] = {
- "node": "Node",
- "resource": "Resource",
- "object": "Object",
- "variant": "Variant",
-}
-# Sync with editor\register_editor_types.cpp
-EDITOR_CLASSES: List[str] = [
- "FileSystemDock",
- "ScriptCreateDialog",
- "ScriptEditor",
- "ScriptEditorBase",
-]
-# Sync with the types mentioned in https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/c_sharp_differences.html
-CLASSES_WITH_CSHARP_DIFFERENCES: List[str] = [
- "@GlobalScope",
- "String",
- "NodePath",
- "Signal",
- "Callable",
- "RID",
- "Basis",
- "Transform2D",
- "Transform3D",
- "Rect2",
- "Rect2i",
- "AABB",
- "Quaternion",
- "Projection",
- "Color",
- "Array",
- "Dictionary",
- "PackedByteArray",
- "PackedColorArray",
- "PackedFloat32Array",
- "PackedFloat64Array",
- "PackedInt32Array",
- "PackedInt64Array",
- "PackedStringArray",
- "PackedVector2Array",
- "PackedVector3Array",
- "Variant",
-]
-
-
-class State:
- def __init__(self) -> None:
- self.num_errors = 0
- self.num_warnings = 0
- self.classes: OrderedDict[str, ClassDef] = OrderedDict()
- self.current_class: str = ""
-
- def parse_class(self, class_root: ET.Element, filepath: str) -> None:
- class_name = class_root.attrib["name"]
- self.current_class = class_name
-
- class_def = ClassDef(class_name)
- self.classes[class_name] = class_def
- class_def.filepath = filepath
-
- inherits = class_root.get("inherits")
- if inherits is not None:
- class_def.inherits = inherits
-
- brief_desc = class_root.find("brief_description")
- if brief_desc is not None and brief_desc.text:
- class_def.brief_description = brief_desc.text
-
- desc = class_root.find("description")
- if desc is not None and desc.text:
- class_def.description = desc.text
-
- properties = class_root.find("members")
- if properties is not None:
- for property in properties:
- assert property.tag == "member"
-
- property_name = property.attrib["name"]
- if property_name in class_def.properties:
- print_error(f'{class_name}.xml: Duplicate property "{property_name}".', self)
- continue
-
- 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 = f"``{default_value}``"
- overrides = property.get("overrides") or None
-
- property_def = PropertyDef(
- property_name, type_name, setter, getter, property.text, default_value, overrides
- )
- 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 = self.parse_params(constructor, "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)
- method_def.definition_name = "constructor"
- 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:
- assert method.tag == "method"
-
- method_name = method.attrib["name"]
- qualifiers = method.get("qualifiers")
-
- return_element = method.find("return")
- if return_element is not None:
- return_type = TypeName.from_element(return_element)
-
- else:
- return_type = TypeName("void")
-
- params = self.parse_params(method, "method")
-
- desc_element = method.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.methods:
- class_def.methods[method_name] = []
-
- 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 = self.parse_params(operator, "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)
- method_def.definition_name = "operator"
- 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:
- assert constant.tag == "constant"
-
- constant_name = constant.attrib["name"]
- value = constant.attrib["value"]
- enum = constant.get("enum")
- is_bitfield = constant.get("is_bitfield") == "true"
- constant_def = ConstantDef(constant_name, value, constant.text, is_bitfield)
- if enum is None:
- if constant_name in class_def.constants:
- print_error(f'{class_name}.xml: Duplicate constant "{constant_name}".', self)
- continue
-
- class_def.constants[constant_name] = constant_def
-
- else:
- if enum in class_def.enums:
- enum_def = class_def.enums[enum]
-
- else:
- enum_def = EnumDef(enum, TypeName("int", enum), is_bitfield)
- class_def.enums[enum] = enum_def
-
- enum_def.values[constant_name] = constant_def
-
- annotations = class_root.find("annotations")
- if annotations is not None:
- for annotation in annotations:
- assert annotation.tag == "annotation"
-
- annotation_name = annotation.attrib["name"]
- qualifiers = annotation.get("qualifiers")
-
- params = self.parse_params(annotation, "annotation")
-
- desc_element = annotation.find("description")
- annotation_desc = None
- if desc_element is not None:
- annotation_desc = desc_element.text
-
- annotation_def = AnnotationDef(annotation_name, params, annotation_desc, qualifiers)
- if annotation_name not in class_def.annotations:
- class_def.annotations[annotation_name] = []
-
- class_def.annotations[annotation_name].append(annotation_def)
-
- signals = class_root.find("signals")
- if signals is not None:
- for signal in signals:
- assert signal.tag == "signal"
-
- signal_name = signal.attrib["name"]
-
- if signal_name in class_def.signals:
- print_error(f'{class_name}.xml: Duplicate signal "{signal_name}".', self)
- continue
-
- params = self.parse_params(signal, "signal")
-
- desc_element = signal.find("description")
- signal_desc = None
- if desc_element is not None:
- signal_desc = desc_element.text
-
- signal_def = SignalDef(signal_name, params, signal_desc)
- class_def.signals[signal_name] = signal_def
-
- theme_items = class_root.find("theme_items")
- if theme_items is not None:
- 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(
- f'{class_name}.xml: Duplicate theme item "{theme_item_name}" of type "{theme_item_data_name}".',
- self,
- )
- continue
-
- default_value = theme_item.get("default") or None
- if default_value is not None:
- default_value = f"``{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:
- for link in tutorials:
- assert link.tag == "link"
-
- if link.text is not None:
- class_def.tutorials.append((link.text.strip(), link.get("title", "")))
-
- self.current_class = ""
-
- def parse_params(self, root: ET.Element, context: str) -> List["ParameterDef"]:
- param_elements = root.findall("param")
- params: Any = [None] * len(param_elements)
-
- for param_index, param_element in enumerate(param_elements):
- param_name = param_element.attrib["name"]
- index = int(param_element.attrib["index"])
- type_name = TypeName.from_element(param_element)
- default = param_element.get("default")
-
- if param_name.strip() == "" or param_name.startswith("_unnamed_arg"):
- print_error(
- f'{self.current_class}.xml: Empty argument name in {context} "{root.attrib["name"]}" at position {param_index}.',
- self,
- )
-
- params[index] = ParameterDef(param_name, type_name, default)
-
- cast: List[ParameterDef] = params
-
- return cast
-
- def sort_classes(self) -> None:
- self.classes = OrderedDict(sorted(self.classes.items(), key=lambda t: t[0].lower()))
-
-
-class TypeName:
- def __init__(self, type_name: str, enum: Optional[str] = None, is_bitfield: bool = False) -> None:
- self.type_name = type_name
- self.enum = enum
- self.is_bitfield = is_bitfield
-
- def to_rst(self, state: State) -> str:
- if self.enum is not None:
- return make_enum(self.enum, self.is_bitfield, state)
- elif self.type_name == "void":
- return "void"
- else:
- return make_type(self.type_name, state)
-
- @classmethod
- def from_element(cls, element: ET.Element) -> "TypeName":
- return cls(element.attrib["type"], element.get("enum"), element.get("is_bitfield") == "true")
-
-
-class DefinitionBase:
- def __init__(
- self,
- definition_name: str,
- name: str,
- ) -> None:
- self.definition_name = definition_name
- self.name = name
-
-
-class PropertyDef(DefinitionBase):
- def __init__(
- self,
- name: str,
- type_name: TypeName,
- setter: Optional[str],
- getter: Optional[str],
- text: Optional[str],
- default_value: Optional[str],
- overrides: Optional[str],
- ) -> None:
- super().__init__("property", name)
-
- self.type_name = type_name
- self.setter = setter
- self.getter = getter
- self.text = text
- self.default_value = default_value
- self.overrides = overrides
-
-
-class ParameterDef(DefinitionBase):
- def __init__(self, name: str, type_name: TypeName, default_value: Optional[str]) -> None:
- super().__init__("parameter", name)
-
- self.type_name = type_name
- self.default_value = default_value
-
-
-class SignalDef(DefinitionBase):
- def __init__(self, name: str, parameters: List[ParameterDef], description: Optional[str]) -> None:
- super().__init__("signal", name)
-
- self.parameters = parameters
- self.description = description
-
-
-class AnnotationDef(DefinitionBase):
- def __init__(
- self,
- name: str,
- parameters: List[ParameterDef],
- description: Optional[str],
- qualifiers: Optional[str],
- ) -> None:
- super().__init__("annotation", name)
-
- self.parameters = parameters
- self.description = description
- self.qualifiers = qualifiers
-
-
-class MethodDef(DefinitionBase):
- def __init__(
- self,
- name: str,
- return_type: TypeName,
- parameters: List[ParameterDef],
- description: Optional[str],
- qualifiers: Optional[str],
- ) -> None:
- super().__init__("method", name)
-
- self.return_type = return_type
- self.parameters = parameters
- self.description = description
- self.qualifiers = qualifiers
-
-
-class ConstantDef(DefinitionBase):
- def __init__(self, name: str, value: str, text: Optional[str], bitfield: bool) -> None:
- super().__init__("constant", name)
-
- self.value = value
- self.text = text
- self.is_bitfield = bitfield
-
-
-class EnumDef(DefinitionBase):
- def __init__(self, name: str, type_name: TypeName, bitfield: bool) -> None:
- super().__init__("enum", name)
-
- self.type_name = type_name
- self.values: OrderedDict[str, ConstantDef] = OrderedDict()
- self.is_bitfield = bitfield
-
-
-class ThemeItemDef(DefinitionBase):
- def __init__(
- self, name: str, type_name: TypeName, data_name: str, text: Optional[str], default_value: Optional[str]
- ) -> None:
- super().__init__("theme item", name)
-
- self.type_name = type_name
- self.data_name = data_name
- self.text = text
- self.default_value = default_value
-
-
-class ClassDef(DefinitionBase):
- def __init__(self, name: str) -> None:
- super().__init__("class", name)
-
- self.constants: OrderedDict[str, ConstantDef] = OrderedDict()
- self.enums: OrderedDict[str, EnumDef] = OrderedDict()
- self.properties: OrderedDict[str, PropertyDef] = OrderedDict()
- self.constructors: OrderedDict[str, List[MethodDef]] = OrderedDict()
- self.methods: OrderedDict[str, List[MethodDef]] = OrderedDict()
- self.operators: OrderedDict[str, List[MethodDef]] = OrderedDict()
- self.signals: OrderedDict[str, SignalDef] = OrderedDict()
- self.annotations: OrderedDict[str, List[AnnotationDef]] = OrderedDict()
- self.theme_items: OrderedDict[str, ThemeItemDef] = OrderedDict()
- self.inherits: Optional[str] = None
- self.brief_description: Optional[str] = None
- self.description: Optional[str] = None
- self.tutorials: List[Tuple[str, str]] = []
-
- # Used to match the class with XML source for output filtering purposes.
- self.filepath: str = ""
-
-
-# Entry point for the RST generator.
-def main() -> None:
- # Enable ANSI escape code support on Windows 10 and later (for colored console output).
- #
- if platform.system().lower() == "windows":
- from ctypes import windll, c_int, byref # type: ignore
-
- stdout_handle = windll.kernel32.GetStdHandle(c_int(-11))
- mode = c_int(0)
- windll.kernel32.GetConsoleMode(c_int(stdout_handle), byref(mode))
- mode = c_int(mode.value | 4)
- windll.kernel32.SetConsoleMode(c_int(stdout_handle), mode)
-
- 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.")
- parser.add_argument(
- "--color",
- action="store_true",
- help="If passed, force colored output even if stdout is not a TTY (useful for continuous integration).",
- )
- group = parser.add_mutually_exclusive_group()
- group.add_argument("--output", "-o", default=".", help="The directory to save output .rst files in.")
- group.add_argument(
- "--dry-run",
- action="store_true",
- help="If passed, no output will be generated and XML files are only checked for errors.",
- )
- args = parser.parse_args()
-
- should_color = args.color or (hasattr(sys.stdout, "isatty") and sys.stdout.isatty())
- STYLES["red"] = "\x1b[91m" if should_color else ""
- STYLES["green"] = "\x1b[92m" if should_color else ""
- STYLES["yellow"] = "\x1b[93m" if should_color else ""
- STYLES["bold"] = "\x1b[1m" if should_color else ""
- STYLES["regular"] = "\x1b[22m" if should_color else ""
- STYLES["reset"] = "\x1b[0m" if should_color else ""
-
- # 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 # type: ignore
- 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(f'No PO file at "{lang_file}" for language "{args.lang}".')
-
- print("Checking for errors in the XML class reference...")
-
- file_list: List[str] = []
-
- for path in args.path:
- # Cut off trailing slashes so os.path.basename doesn't choke.
- if path.endswith("/") or path.endswith("\\"):
- path = path[:-1]
-
- if os.path.basename(path) in ["modules", "platform"]:
- for subdir, dirs, _ in os.walk(path):
- if "doc_classes" in dirs:
- doc_dir = os.path.join(subdir, "doc_classes")
- class_file_names = (f for f in os.listdir(doc_dir) if f.endswith(".xml"))
- file_list += (os.path.join(doc_dir, f) for f in class_file_names)
-
- elif os.path.isdir(path):
- file_list += (os.path.join(path, f) for f in os.listdir(path) if f.endswith(".xml"))
-
- elif os.path.isfile(path):
- if not path.endswith(".xml"):
- print(f'Got non-.xml file "{path}" in input, skipping.')
- continue
-
- file_list.append(path)
-
- classes: Dict[str, Tuple[ET.Element, str]] = {}
- state = State()
-
- for cur_file in file_list:
- try:
- tree = ET.parse(cur_file)
- except ET.ParseError as e:
- print_error(f"{cur_file}: Parse error while reading the file: {e}", state)
- continue
- doc = tree.getroot()
-
- name = doc.attrib["name"]
- if name in classes:
- print_error(f'{cur_file}: Duplicate class "{name}".', state)
- continue
-
- classes[name] = (doc, cur_file)
-
- for name, data in classes.items():
- try:
- state.parse_class(data[0], data[1])
- except Exception as e:
- print_error(f"{name}.xml: Exception while parsing class: {e}", state)
-
- state.sort_classes()
-
- pattern = re.compile(args.filter)
-
- # Create the output folder recursively if it doesn't already exist.
- os.makedirs(args.output, exist_ok=True)
-
- print("Generating the RST class reference...")
-
- grouped_classes: Dict[str, List[str]] = {}
-
- for class_name, class_def in state.classes.items():
- if args.filter and not pattern.search(class_def.filepath):
- continue
- state.current_class = class_name
- make_rst_class(class_def, state, args.dry_run, args.output)
-
- group_name = get_class_group(class_def, state)
-
- if group_name not in grouped_classes:
- grouped_classes[group_name] = []
- grouped_classes[group_name].append(class_name)
-
- if is_editor_class(class_def):
- if "editor" not in grouped_classes:
- grouped_classes["editor"] = []
- grouped_classes["editor"].append(class_name)
-
- print("")
- print("Generating the index file...")
-
- make_rst_index(grouped_classes, args.dry_run, args.output)
-
- print("")
-
- if state.num_warnings >= 2:
- print(
- f'{STYLES["yellow"]}{state.num_warnings} warnings were found in the class reference XML. Please check the messages above.{STYLES["reset"]}'
- )
- elif state.num_warnings == 1:
- print(
- f'{STYLES["yellow"]}1 warning was found in the class reference XML. Please check the messages above.{STYLES["reset"]}'
- )
-
- if state.num_errors == 0:
- print(f'{STYLES["green"]}No errors found in the class reference XML.{STYLES["reset"]}')
- if not args.dry_run:
- print(f"Wrote reStructuredText files for each class to: {args.output}")
- else:
- if state.num_errors >= 2:
- print(
- f'{STYLES["red"]}{state.num_errors} errors were found in the class reference XML. Please check the messages above.{STYLES["reset"]}'
- )
- else:
- print(
- f'{STYLES["red"]}1 error was found in the class reference XML. Please check the messages above.{STYLES["reset"]}'
- )
- exit(1)
-
-
-# Common helpers.
-
-
-def print_error(error: str, state: State) -> None:
- print(f'{STYLES["red"]}{STYLES["bold"]}ERROR:{STYLES["regular"]} {error}{STYLES["reset"]}')
- state.num_errors += 1
-
-
-def print_warning(warning: str, state: State) -> None:
- print(f'{STYLES["yellow"]}{STYLES["bold"]}WARNING:{STYLES["regular"]} {warning}{STYLES["reset"]}')
- state.num_warnings += 1
-
-
-def translate(string: 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 get_git_branch() -> str:
- if hasattr(version, "docs") and version.docs != "latest":
- return version.docs
-
- return "master"
-
-
-def get_class_group(class_def: ClassDef, state: State) -> str:
- group_name = "node"
- class_name = class_def.name
-
- if class_name.startswith("@"):
- group_name = "global"
- elif class_def.inherits:
- inherits = class_def.inherits.strip()
-
- while inherits in state.classes:
- if inherits == "Node":
- group_name = "node"
- break
- if inherits == "Resource":
- group_name = "resource"
- break
- if inherits == "Object":
- group_name = "object"
- break
-
- inode = state.classes[inherits].inherits
- if inode:
- inherits = inode.strip()
- else:
- break
-
- # Assume the group
- if inherits == "Node":
- group_name = "node"
- if inherits == "Resource":
- group_name = "resource"
- if inherits == "Object":
- group_name = "object"
- if inherits == "RefCounted":
- group_name = "object"
-
- return group_name
-
-
-def is_editor_class(class_def: ClassDef) -> bool:
- class_name = class_def.name
-
- if class_name.startswith("Editor"):
- return True
- if class_name in EDITOR_CLASSES:
- return True
-
- return False
-
-
-# Generator methods.
-
-
-def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir: str) -> None:
- class_name = class_def.name
-
- if dry_run:
- f = open(os.devnull, "w", encoding="utf-8")
- else:
- f = open(os.path.join(output_dir, f"class_{class_name.lower()}.rst"), "w", encoding="utf-8")
-
- # Remove the "Edit on Github" button from the online docs page.
- f.write(":github_url: hide\n\n")
-
- # Warn contributors not to edit this file directly.
- # Also provide links to the source files for reference.
-
- git_branch = get_git_branch()
- source_xml_path = os.path.relpath(class_def.filepath, root_directory).replace("\\", "/")
- source_github_url = f"https://github.com/godotengine/godot/tree/{git_branch}/{source_xml_path}"
- generator_github_url = f"https://github.com/godotengine/godot/tree/{git_branch}/doc/tools/make_rst.py"
-
- f.write(".. DO NOT EDIT THIS FILE!!!\n")
- f.write(".. Generated automatically from Godot engine sources.\n")
- f.write(f".. Generator: {generator_github_url}.\n")
- f.write(f".. XML source: {source_github_url}.\n\n")
-
- # Document reference id and header.
- f.write(f".. _class_{class_name}:\n\n")
- f.write(make_heading(class_name, "=", False))
-
- ### INHERITANCE TREE ###
-
- # Ascendants
- if class_def.inherits:
- inherits = class_def.inherits.strip()
- f.write(f'**{translate("Inherits:")}** ')
- first = True
- while inherits in state.classes:
- if not first:
- f.write(" **<** ")
- else:
- first = False
-
- f.write(make_type(inherits, state))
- inode = state.classes[inherits].inherits
- if inode:
- inherits = inode.strip()
- else:
- break
-
- # Assume we inherit from a Godot class if not found
- if inherits not in state.classes:
- if not first:
- f.write(" **<** ")
- f.write(make_type(inherits, state))
-
- f.write("\n\n")
-
- # Descendants
- inherited: List[str] = []
- for c in state.classes.values():
- if c.inherits and c.inherits.strip() == class_name:
- inherited.append(c.name)
-
- if len(inherited):
- f.write(f'**{translate("Inherited By:")}** ')
- for i, child in enumerate(inherited):
- if i > 0:
- f.write(", ")
- f.write(make_type(child, state))
- f.write("\n\n")
-
- ### INTRODUCTION ###
-
- has_description = False
-
- # Brief description
- if class_def.brief_description is not None and class_def.brief_description.strip() != "":
- has_description = True
-
- f.write(f"{format_text_block(class_def.brief_description.strip(), class_def, state)}\n\n")
-
- # Class description
- if class_def.description is not None and class_def.description.strip() != "":
- has_description = True
-
- f.write(".. rst-class:: classref-introduction-group\n\n")
- f.write(make_heading("Description", "-"))
-
- f.write(f"{format_text_block(class_def.description.strip(), class_def, state)}\n\n")
-
- if not has_description:
- f.write(".. container:: contribute\n\n\t")
- f.write(
- translate(
- "There is currently no description for this class. Please help us by :ref:`contributing one `!"
- )
- + "\n\n"
- )
-
- if class_def.name in CLASSES_WITH_CSHARP_DIFFERENCES:
- f.write(".. note::\n\n\t")
- f.write(
- translate(
- "There are notable differences when using this API with C#. See :ref:`doc_c_sharp_differences` for more information."
- )
- + "\n\n"
- )
-
- # Online tutorials
- if len(class_def.tutorials) > 0:
- f.write(".. rst-class:: classref-introduction-group\n\n")
- f.write(make_heading("Tutorials", "-"))
-
- for url, title in class_def.tutorials:
- f.write(f"- {make_link(url, title)}\n\n")
-
- ### REFERENCE TABLES ###
-
- # Reused container for reference tables.
- ml: List[Tuple[Optional[str], ...]] = []
-
- # Properties reference table
- if len(class_def.properties) > 0:
- f.write(".. rst-class:: classref-reftable-group\n\n")
- f.write(make_heading("Properties", "-"))
-
- ml = []
- 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.overrides:
- ref = f":ref:`{property_def.overrides}`"
- # Not using translate() for now as it breaks table formatting.
- ml.append((type_rst, property_def.name, f"{default} (overrides {ref})"))
- else:
- ref = f":ref:`{property_def.name}`"
- ml.append((type_rst, ref, default))
-
- format_table(f, ml, True)
-
- # Constructors, Methods, Operators reference tables
- if len(class_def.constructors) > 0:
- f.write(".. rst-class:: classref-reftable-group\n\n")
- 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(".. rst-class:: classref-reftable-group\n\n")
- 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, "method", state))
-
- format_table(f, ml)
-
- if len(class_def.operators) > 0:
- f.write(".. rst-class:: classref-reftable-group\n\n")
- 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 reference table
- if len(class_def.theme_items) > 0:
- f.write(".. rst-class:: classref-reftable-group\n\n")
- f.write(make_heading("Theme Properties", "-"))
-
- ml = []
- for theme_item_def in class_def.theme_items.values():
- ref = f":ref:`{theme_item_def.name}`"
- ml.append((theme_item_def.type_name.to_rst(state), ref, theme_item_def.default_value))
-
- format_table(f, ml, True)
-
- ### DETAILED DESCRIPTIONS ###
-
- # Signal descriptions
- if len(class_def.signals) > 0:
- f.write(make_separator(True))
- f.write(".. rst-class:: classref-descriptions-group\n\n")
- f.write(make_heading("Signals", "-"))
-
- index = 0
-
- for signal in class_def.signals.values():
- if index != 0:
- f.write(make_separator())
-
- # Create signal signature and anchor point.
-
- f.write(f".. _class_{class_name}_signal_{signal.name}:\n\n")
- f.write(".. rst-class:: classref-signal\n\n")
-
- _, signature = make_method_signature(class_def, signal, "", state)
- f.write(f"{signature}\n\n")
-
- # Add signal description, or a call to action if it's missing.
-
- if signal.description is not None and signal.description.strip() != "":
- f.write(f"{format_text_block(signal.description.strip(), signal, state)}\n\n")
- else:
- f.write(".. container:: contribute\n\n\t")
- f.write(
- translate(
- "There is currently no description for this signal. Please help us by :ref:`contributing one `!"
- )
- + "\n\n"
- )
-
- index += 1
-
- # Enumeration descriptions
- if len(class_def.enums) > 0:
- f.write(make_separator(True))
- f.write(".. rst-class:: classref-descriptions-group\n\n")
- f.write(make_heading("Enumerations", "-"))
-
- index = 0
-
- for e in class_def.enums.values():
- if index != 0:
- f.write(make_separator())
-
- # Create enumeration signature and anchor point.
-
- f.write(f".. _enum_{class_name}_{e.name}:\n\n")
- f.write(".. rst-class:: classref-enumeration\n\n")
-
- if e.is_bitfield:
- f.write(f"flags **{e.name}**:\n\n")
- else:
- f.write(f"enum **{e.name}**:\n\n")
-
- for value in e.values.values():
- # Also create signature and anchor point for each enum constant.
-
- f.write(f".. _class_{class_name}_constant_{value.name}:\n\n")
- f.write(".. rst-class:: classref-enumeration-constant\n\n")
-
- f.write(f"{e.type_name.to_rst(state)} **{value.name}** = ``{value.value}``\n\n")
-
- # Add enum constant description.
-
- if value.text is not None and value.text.strip() != "":
- f.write(f"{format_text_block(value.text.strip(), value, state)}")
-
- f.write("\n\n")
-
- index += 1
-
- # Constant descriptions
- if len(class_def.constants) > 0:
- f.write(make_separator(True))
- f.write(".. rst-class:: classref-descriptions-group\n\n")
- f.write(make_heading("Constants", "-"))
-
- for constant in class_def.constants.values():
- # Create constant signature and anchor point.
-
- f.write(f".. _class_{class_name}_constant_{constant.name}:\n\n")
- f.write(".. rst-class:: classref-constant\n\n")
-
- f.write(f"**{constant.name}** = ``{constant.value}``\n\n")
-
- # Add enum constant description.
-
- if constant.text is not None and constant.text.strip() != "":
- f.write(f"{format_text_block(constant.text.strip(), constant, state)}")
-
- f.write("\n\n")
-
- # Annotation descriptions
- if len(class_def.annotations) > 0:
- f.write(make_separator(True))
- f.write(make_heading("Annotations", "-"))
-
- index = 0
-
- for method_list in class_def.annotations.values(): # type: ignore
- for i, m in enumerate(method_list):
- if index != 0:
- f.write(make_separator())
-
- # Create annotation signature and anchor point.
-
- if i == 0:
- f.write(f".. _class_{class_name}_annotation_{m.name}:\n\n")
-
- f.write(".. rst-class:: classref-annotation\n\n")
-
- _, signature = make_method_signature(class_def, m, "", state)
- f.write(f"{signature}\n\n")
-
- # Add annotation description, or a call to action if it's missing.
-
- if m.description is not None and m.description.strip() != "":
- f.write(f"{format_text_block(m.description.strip(), m, state)}\n\n")
- else:
- f.write(".. container:: contribute\n\n\t")
- f.write(
- translate(
- "There is currently no description for this annotation. Please help us by :ref:`contributing one `!"
- )
- + "\n\n"
- )
-
- index += 1
-
- # Property descriptions
- if any(not p.overrides for p in class_def.properties.values()) > 0:
- f.write(make_separator(True))
- f.write(".. rst-class:: classref-descriptions-group\n\n")
- f.write(make_heading("Property Descriptions", "-"))
-
- index = 0
-
- for property_def in class_def.properties.values():
- if property_def.overrides:
- continue
-
- if index != 0:
- f.write(make_separator())
-
- # Create property signature and anchor point.
-
- f.write(f".. _class_{class_name}_property_{property_def.name}:\n\n")
- f.write(".. rst-class:: classref-property\n\n")
-
- property_default = ""
- if property_def.default_value is not None:
- property_default = f" = {property_def.default_value}"
- f.write(f"{property_def.type_name.to_rst(state)} **{property_def.name}**{property_default}\n\n")
-
- # Create property setter and getter records.
-
- property_setget = ""
-
- if property_def.setter is not None and not property_def.setter.startswith("_"):
- property_setter = make_setter_signature(class_def, property_def, state)
- property_setget += f"- {property_setter}\n"
-
- if property_def.getter is not None and not property_def.getter.startswith("_"):
- property_getter = make_getter_signature(class_def, property_def, state)
- property_setget += f"- {property_getter}\n"
-
- if property_setget != "":
- f.write(".. rst-class:: classref-property-setget\n\n")
- f.write(property_setget)
- f.write("\n")
-
- # Add property description, or a call to action if it's missing.
-
- if property_def.text is not None and property_def.text.strip() != "":
- f.write(f"{format_text_block(property_def.text.strip(), property_def, state)}\n\n")
- else:
- f.write(".. container:: contribute\n\n\t")
- f.write(
- translate(
- "There is currently no description for this property. Please help us by :ref:`contributing one `!"
- )
- + "\n\n"
- )
-
- index += 1
-
- # Constructor, Method, Operator descriptions
- if len(class_def.constructors) > 0:
- f.write(make_separator(True))
- f.write(".. rst-class:: classref-descriptions-group\n\n")
- 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(make_separator())
-
- # Create constructor signature and anchor point.
-
- if i == 0:
- f.write(f".. _class_{class_name}_constructor_{m.name}:\n\n")
-
- f.write(".. rst-class:: classref-constructor\n\n")
-
- ret_type, signature = make_method_signature(class_def, m, "", state)
- f.write(f"{ret_type} {signature}\n\n")
-
- # Add constructor description, or a call to action if it's missing.
-
- if m.description is not None and m.description.strip() != "":
- f.write(f"{format_text_block(m.description.strip(), m, state)}\n\n")
- else:
- f.write(".. container:: contribute\n\n\t")
- f.write(
- translate(
- "There is currently no description for this constructor. Please help us by :ref:`contributing one `!"
- )
- + "\n\n"
- )
-
- index += 1
-
- if len(class_def.methods) > 0:
- f.write(make_separator(True))
- f.write(".. rst-class:: classref-descriptions-group\n\n")
- 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(make_separator())
-
- # Create method signature and anchor point.
-
- if i == 0:
- f.write(f".. _class_{class_name}_method_{m.name}:\n\n")
-
- f.write(".. rst-class:: classref-method\n\n")
-
- ret_type, signature = make_method_signature(class_def, m, "", state)
- f.write(f"{ret_type} {signature}\n\n")
-
- # Add method description, or a call to action if it's missing.
-
- if m.description is not None and m.description.strip() != "":
- f.write(f"{format_text_block(m.description.strip(), m, state)}\n\n")
- else:
- f.write(".. container:: contribute\n\n\t")
- f.write(
- translate(
- "There is currently no description for this method. Please help us by :ref:`contributing one `!"
- )
- + "\n\n"
- )
-
- index += 1
-
- if len(class_def.operators) > 0:
- f.write(make_separator(True))
- f.write(".. rst-class:: classref-descriptions-group\n\n")
- 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(make_separator())
-
- # Create operator signature and anchor point.
-
- operator_anchor = f".. _class_{class_name}_operator_{sanitize_operator_name(m.name, state)}"
- for parameter in m.parameters:
- operator_anchor += f"_{parameter.type_name.type_name}"
- operator_anchor += f":\n\n"
- f.write(operator_anchor)
-
- f.write(".. rst-class:: classref-operator\n\n")
-
- ret_type, signature = make_method_signature(class_def, m, "", state)
- f.write(f"{ret_type} {signature}\n\n")
-
- # Add operator description, or a call to action if it's missing.
-
- if m.description is not None and m.description.strip() != "":
- f.write(f"{format_text_block(m.description.strip(), m, state)}\n\n")
- else:
- f.write(".. container:: contribute\n\n\t")
- f.write(
- translate(
- "There is currently no description for this operator. Please help us by :ref:`contributing one `!"
- )
- + "\n\n"
- )
-
- index += 1
-
- # Theme property descriptions
- if len(class_def.theme_items) > 0:
- f.write(make_separator(True))
- f.write(".. rst-class:: classref-descriptions-group\n\n")
- f.write(make_heading("Theme Property Descriptions", "-"))
-
- index = 0
-
- for theme_item_def in class_def.theme_items.values():
- if index != 0:
- f.write(make_separator())
-
- # Create theme property signature and anchor point.
-
- f.write(f".. _class_{class_name}_theme_{theme_item_def.data_name}_{theme_item_def.name}:\n\n")
- f.write(".. rst-class:: classref-themeproperty\n\n")
-
- theme_item_default = ""
- if theme_item_def.default_value is not None:
- theme_item_default = f" = {theme_item_def.default_value}"
- f.write(f"{theme_item_def.type_name.to_rst(state)} **{theme_item_def.name}**{theme_item_default}\n\n")
-
- # Add theme property description, or a call to action if it's missing.
-
- if theme_item_def.text is not None and theme_item_def.text.strip() != "":
- f.write(f"{format_text_block(theme_item_def.text.strip(), theme_item_def, state)}\n\n")
- else:
- f.write(".. container:: contribute\n\n\t")
- f.write(
- translate(
- "There is currently no description for this theme property. Please help us by :ref:`contributing one `!"
- )
- + "\n\n"
- )
-
- index += 1
-
- f.write(make_footer())
-
-
-def make_type(klass: str, state: 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]
- if link_type in state.classes:
- return f":ref:`{klass}`"
- # Assume failures should link to Godot docs
- return f"`{link_type} `_"
- #print_error(f'{state.current_class}.xml: Unresolved type "{klass}".', state)
- #return klass
-
-
-def make_enum(t: str, is_bitfield: bool, state: State) -> str:
- p = t.find(".")
- if p >= 0:
- c = t[0:p]
- e = t[p + 1 :]
- # Variant enums live in GlobalScope but still use periods.
- if c == "Variant":
- c = "@GlobalScope"
- e = "Variant." + e
- else:
- c = state.current_class
- e = t
- if c in state.classes and e not in state.classes[c].enums:
- c = "@GlobalScope"
-
- if c in state.classes and e in state.classes[c].enums:
- if is_bitfield:
- if not state.classes[c].enums[e].is_bitfield:
- print_error(f'{state.current_class}.xml: Enum "{t}" is not bitfield.', state)
- return f"|bitfield|\<:ref:`{e}`\>"
- else:
- return f":ref:`{e}`"
-
- # HashingContext.HashType
- # https://docs.godotengine.org/en/stable/classes/class_hashingcontext.html#enum-hashingcontext-hashtype
- parts = t.split(".")
- if len(parts) > 1:
- class_name = parts[0]
- child = parts[1]
- return f"`{t} `_"
-
- # Assume failures will link to Godot docs
- return f"`{t} `_"
-
- # Don't fail for `Vector3.Axis`, as this enum is a special case which is expected not to be resolved.
- if f"{c}.{e}" != "Vector3.Axis":
- print_error(f'{state.current_class}.xml: Unresolved enum "{t}".', state)
-
- return t
-
-
-def make_method_signature(
- class_def: ClassDef, definition: Union[AnnotationDef, MethodDef, SignalDef], ref_type: str, state: State
-) -> Tuple[str, str]:
- ret_type = ""
-
- if isinstance(definition, MethodDef):
- ret_type = definition.return_type.to_rst(state)
-
- qualifiers = None
- if isinstance(definition, (MethodDef, AnnotationDef)):
- qualifiers = definition.qualifiers
-
- out = ""
- if isinstance(definition, MethodDef) and ref_type != "":
- if ref_type == "operator":
- op_name = definition.name.replace("<", "\\<") # So operator "<" gets correctly displayed.
- out += f":ref:`{op_name}` "
- else:
- out += f":ref:`{definition.name}` "
- else:
- out += f"**{definition.name}** "
-
- out += "**(**"
- for i, arg in enumerate(definition.parameters):
- if i > 0:
- out += ", "
- else:
- out += " "
-
- out += f"{arg.type_name.to_rst(state)} {arg.name}"
-
- if arg.default_value is not None:
- out += f"={arg.default_value}"
-
- if qualifiers is not None and "vararg" in qualifiers:
- if len(definition.parameters) > 0:
- out += ", ..."
- else:
- out += " ..."
-
- out += " **)**"
-
- if qualifiers is not None:
- # Use substitutions for abbreviations. This is used to display tooltips on hover.
- # See `make_footer()` for descriptions.
- for qualifier in qualifiers.split():
- out += f" |{qualifier}|"
-
- return ret_type, out
-
-
-def make_setter_signature(class_def: ClassDef, property_def: PropertyDef, state: State) -> str:
- if property_def.setter is None:
- return ""
-
- # If setter is a method available as a method definition, we use that.
- if property_def.setter in class_def.methods:
- setter = class_def.methods[property_def.setter][0]
- # Otherwise we fake it with the information we have available.
- else:
- setter_params: List[ParameterDef] = []
- setter_params.append(ParameterDef("value", property_def.type_name, None))
- setter = MethodDef(property_def.setter, TypeName("void"), setter_params, None, None)
-
- ret_type, signature = make_method_signature(class_def, setter, "", state)
- return f"{ret_type} {signature}"
-
-
-def make_getter_signature(class_def: ClassDef, property_def: PropertyDef, state: State) -> str:
- if property_def.getter is None:
- return ""
-
- # If getter is a method available as a method definition, we use that.
- if property_def.getter in class_def.methods:
- getter = class_def.methods[property_def.getter][0]
- # Otherwise we fake it with the information we have available.
- else:
- getter_params: List[ParameterDef] = []
- getter = MethodDef(property_def.getter, property_def.type_name, getter_params, None, None)
-
- ret_type, signature = make_method_signature(class_def, getter, "", state)
- return f"{ret_type} {signature}"
-
-
-def make_heading(title: str, underline: str, l10n: bool = True) -> str:
- if l10n:
- new_title = translate(title)
- if new_title != title:
- title = new_title
- underline *= 2 # Double length to handle wide chars.
- return f"{title}\n{(underline * len(title))}\n\n"
-
-
-def make_footer() -> str:
- # Generate reusable abbreviation substitutions.
- # This way, we avoid bloating the generated rST with duplicate abbreviations.
- virtual_msg = translate("This method should typically be overridden by the user to have any effect.")
- const_msg = translate("This method has no side effects. It doesn't modify any of the instance's member variables.")
- vararg_msg = translate("This method accepts any number of arguments after the ones described here.")
- constructor_msg = translate("This method is used to construct a type.")
- static_msg = translate(
- "This method doesn't need an instance to be called, so it can be called directly using the class name."
- )
- operator_msg = translate("This method describes a valid operator to use with this type as left-hand operand.")
- bitfield_msg = translate("This value is an integer composed as a bitmask of the following flags.")
-
- return (
- f".. |virtual| replace:: :abbr:`virtual ({virtual_msg})`\n"
- f".. |const| replace:: :abbr:`const ({const_msg})`\n"
- f".. |vararg| replace:: :abbr:`vararg ({vararg_msg})`\n"
- f".. |constructor| replace:: :abbr:`constructor ({constructor_msg})`\n"
- f".. |static| replace:: :abbr:`static ({static_msg})`\n"
- f".. |operator| replace:: :abbr:`operator ({operator_msg})`\n"
- f".. |bitfield| replace:: :abbr:`BitField ({bitfield_msg})`\n"
- )
-
-
-def make_separator(section_level: bool = False) -> str:
- separator_class = "item"
- if section_level:
- separator_class = "section"
-
- return f".. rst-class:: classref-{separator_class}-separator\n\n----\n\n"
-
-
-def make_link(url: str, title: 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`
- # Or use the title if provided.
- if title != "":
- return f"`{title} <../{groups[0]}.html{groups[1]}>`__"
- return f"`{groups[1]} <../{groups[0]}.html{groups[1]}>`__ in :doc:`../{groups[0]}`"
- elif match.lastindex == 1:
- # Doc reference, for example:
- # `Math`
- if title != "":
- return f":doc:`{title} <../{groups[0]}>`"
- return f":doc:`../{groups[0]}`"
-
- # External link, for example:
- # `http://enet.bespin.org/usergroup0.html`
- if title != "":
- return f"`{title} <{url}>`__"
- return f"`{url} <{url}>`__"
-
-
-def make_rst_index(grouped_classes: Dict[str, List[str]], dry_run: bool, output_dir: str) -> None:
- if dry_run:
- f = open(os.devnull, "w", encoding="utf-8")
- else:
- f = open(os.path.join(output_dir, "index.rst"), "w", encoding="utf-8")
-
- # Remove the "Edit on Github" button from the online docs page.
- f.write(":github_url: hide\n\n")
-
- # Warn contributors not to edit this file directly.
- # Also provide links to the source files for reference.
-
- git_branch = get_git_branch()
- generator_github_url = f"https://github.com/godotengine/godot/tree/{git_branch}/doc/tools/make_rst.py"
-
- f.write(".. DO NOT EDIT THIS FILE!!!\n")
- f.write(".. Generated automatically from Godot engine sources.\n")
- f.write(f".. Generator: {generator_github_url}.\n\n")
-
- f.write(".. _doc_class_reference:\n\n")
-
- main_title = translate("All classes")
- f.write(f"{main_title}\n")
- f.write(f"{'=' * len(main_title)}\n\n")
-
- for group_name in CLASS_GROUPS:
- if group_name in grouped_classes:
- group_title = translate(CLASS_GROUPS[group_name])
-
- f.write(f"{group_title}\n")
- f.write(f"{'=' * len(group_title)}\n\n")
-
- f.write(".. toctree::\n")
- f.write(" :maxdepth: 1\n")
- f.write(f" :name: toc-class-ref-{group_name}s\n")
- f.write("\n")
-
- if group_name in CLASS_GROUPS_BASE:
- f.write(f" class_{CLASS_GROUPS_BASE[group_name].lower()}\n")
-
- for class_name in grouped_classes[group_name]:
- if group_name in CLASS_GROUPS_BASE and CLASS_GROUPS_BASE[group_name].lower() == class_name.lower():
- continue
-
- f.write(f" class_{class_name.lower()}\n")
-
- f.write("\n")
-
-
-# Formatting helpers.
-
-
-RESERVED_FORMATTING_TAGS = ["i", "b", "u", "code", "kbd", "center", "url", "br"]
-RESERVED_CODEBLOCK_TAGS = ["codeblocks", "codeblock", "gdscript", "csharp"]
-RESERVED_CROSSLINK_TAGS = ["method", "member", "signal", "constant", "enum", "annotation", "theme_item", "param"]
-
-
-def is_in_tagset(tag_text: str, tagset: List[str]) -> bool:
- for tag in tagset:
- # Complete match.
- if tag_text == tag:
- return True
- # Tag with arguments.
- if tag_text.startswith(tag + " "):
- return True
- # Tag with arguments, special case for [url].
- if tag_text.startswith(tag + "="):
- return True
-
- return False
-
-
-def format_text_block(
- text: str,
- context: Union[DefinitionBase, None],
- state: State,
-) -> str:
- # Linebreak + tabs in the XML should become two line breaks unless in a "codeblock"
- pos = 0
- while True:
- pos = text.find("\n", pos)
- if pos == -1:
- break
-
- pre_text = text[:pos]
- indent_level = 0
- while pos + 1 < len(text) and text[pos + 1] == "\t":
- pos += 1
- indent_level += 1
- post_text = text[pos + 1 :]
-
- # Handle codeblocks
- if (
- post_text.startswith("[codeblock]")
- or post_text.startswith("[gdscript]")
- or post_text.startswith("[csharp]")
- ):
- block_type = post_text[1:].split("]")[0]
- result = format_codeblock(block_type, post_text, indent_level, state)
- if result is None:
- return ""
- text = f"{pre_text}{result[0]}"
- pos += result[1] - indent_level
-
- # Handle normal text
- else:
- text = f"{pre_text}\n\n{post_text}"
- pos += 2 - indent_level
-
- next_brac_pos = text.find("[")
- text = escape_rst(text, next_brac_pos)
-
- context_name = format_context_name(context)
-
- # Handle [tags]
- inside_code = False
- inside_code_tag = ""
- inside_code_tabs = False
- pos = 0
- tag_depth = 0
- while True:
- pos = text.find("[", pos)
- if pos == -1:
- break
-
- endq_pos = text.find("]", pos + 1)
- if endq_pos == -1:
- break
-
- pre_text = text[:pos]
- post_text = text[endq_pos + 1 :]
- tag_text = text[pos + 1 : endq_pos]
-
- escape_pre = False
- escape_post = False
-
- # Tag is a reference to a class.
- if tag_text in state.classes and not inside_code:
- if tag_text == state.current_class:
- # Don't create a link to the same class, format it as strong emphasis.
- tag_text = f"**{tag_text}**"
- else:
- tag_text = make_type(tag_text, state)
- escape_pre = True
- escape_post = True
-
- # Tag is a cross-reference or a formatting directive.
- else:
- cmd = tag_text
- space_pos = tag_text.find(" ")
-
- # Anything identified as a tag inside of a code block is valid,
- # unless it's a matching closing tag.
- if inside_code:
- # Exiting codeblocks and inline code tags.
-
- if inside_code_tag == cmd[1:]:
- if cmd == "/codeblock" or cmd == "/gdscript" or cmd == "/csharp":
- tag_text = ""
- tag_depth -= 1
- inside_code = False
- # Strip newline if the tag was alone on one
- if pre_text[-1] == "\n":
- pre_text = pre_text[:-1]
-
- elif cmd == "/code":
- tag_text = "``"
- tag_depth -= 1
- inside_code = False
- escape_post = True
-
- else:
- if cmd.startswith("/"):
- print_warning(
- f'{state.current_class}.xml: Potential error inside of a code tag, found a string that looks like a closing tag "[{cmd}]" in {context_name}.',
- state,
- )
-
- tag_text = f"[{tag_text}]"
-
- # Entering codeblocks and inline code tags.
-
- elif cmd == "codeblocks":
- tag_depth += 1
- tag_text = "\n.. tabs::"
- inside_code_tabs = True
- elif cmd == "/codeblocks":
- tag_depth -= 1
- tag_text = ""
- inside_code_tabs = False
-
- elif cmd == "codeblock" or cmd == "gdscript" or cmd == "csharp":
- tag_depth += 1
-
- if cmd == "gdscript":
- if not inside_code_tabs:
- print_error(
- f"{state.current_class}.xml: GDScript code block is used outside of [codeblocks] in {context_name}.",
- state,
- )
- tag_text = "\n .. code-tab:: gdscript\n"
- elif cmd == "csharp":
- if not inside_code_tabs:
- print_error(
- f"{state.current_class}.xml: C# code block is used outside of [codeblocks] in {context_name}.",
- state,
- )
- tag_text = "\n .. code-tab:: csharp\n"
- else:
- tag_text = "\n::\n"
-
- inside_code = True
- inside_code_tag = cmd
-
- elif cmd == "code":
- tag_text = "``"
- tag_depth += 1
- inside_code = True
- inside_code_tag = cmd
- escape_pre = True
-
- valid_context = isinstance(context, (MethodDef, SignalDef, AnnotationDef))
- if valid_context:
- endcode_pos = text.find("[/code]", endq_pos + 1)
- if endcode_pos == -1:
- print_error(
- f"{state.current_class}.xml: Tag depth mismatch for [code]: no closing [/code] in {context_name}.",
- state,
- )
- break
-
- inside_code_text = text[endq_pos + 1 : endcode_pos]
- context_params: List[ParameterDef] = context.parameters # type: ignore
- for param_def in context_params:
- if param_def.name == inside_code_text:
- print_warning(
- f'{state.current_class}.xml: Potential error inside of a code tag, found a string "{inside_code_text}" that matches one of the parameters in {context_name}.',
- state,
- )
- break
-
- # Cross-references to items in this or other class documentation pages.
- elif is_in_tagset(cmd, RESERVED_CROSSLINK_TAGS):
- link_type: str = ""
- link_target: str = ""
- if space_pos >= 0:
- link_type = tag_text[:space_pos]
- link_target = tag_text[space_pos + 1 :].strip()
-
- if link_target == "":
- print_error(
- f'{state.current_class}.xml: Empty cross-reference link "{cmd}" in {context_name}.',
- state,
- )
- tag_text = ""
- else:
- if (
- cmd.startswith("method")
- or cmd.startswith("constructor")
- or cmd.startswith("operator")
- or cmd.startswith("member")
- or cmd.startswith("signal")
- or cmd.startswith("annotation")
- or cmd.startswith("theme_item")
- or cmd.startswith("constant")
- ):
- if link_target.find(".") != -1:
- ss = link_target.split(".")
- if len(ss) > 2:
- print_error(
- f'{state.current_class}.xml: Bad reference "{link_target}" in {context_name}.',
- state,
- )
- class_param, method_param = ss
-
- else:
- class_param = state.current_class
- method_param = link_target
-
- # Default to the tag command name. This works by default for most tags,
- # but member and theme_item have special cases.
- ref_type = "_{}".format(link_type)
- if link_type == "member":
- ref_type = "_property"
-
- if class_param in state.classes:
- class_def = state.classes[class_param]
-
- if cmd.startswith("method") and method_param not in class_def.methods:
- print_error(
- f'{state.current_class}.xml: Unresolved method reference "{link_target}" in {context_name}.',
- state,
- )
-
- elif cmd.startswith("constructor") and method_param not in class_def.constructors:
- print_error(
- f'{state.current_class}.xml: Unresolved constructor reference "{link_target}" in {context_name}.',
- state,
- )
-
- elif cmd.startswith("operator") and method_param not in class_def.operators:
- print_error(
- f'{state.current_class}.xml: Unresolved operator reference "{link_target}" in {context_name}.',
- state,
- )
-
- elif cmd.startswith("member") and method_param not in class_def.properties:
- print_error(
- f'{state.current_class}.xml: Unresolved member reference "{link_target}" in {context_name}.',
- state,
- )
-
- elif cmd.startswith("signal") and method_param not in class_def.signals:
- print_error(
- f'{state.current_class}.xml: Unresolved signal reference "{link_target}" in {context_name}.',
- state,
- )
-
- elif cmd.startswith("annotation") and method_param not in class_def.annotations:
- print_error(
- f'{state.current_class}.xml: Unresolved annotation reference "{link_target}" in {context_name}.',
- state,
- )
-
- elif cmd.startswith("theme_item"):
- if method_param not in class_def.theme_items:
- print_error(
- f'{state.current_class}.xml: Unresolved theme item reference "{link_target}" in {context_name}.',
- state,
- )
- else:
- # Needs theme data type to be properly linked, which we cannot get without a class.
- name = class_def.theme_items[method_param].data_name
- ref_type = f"_theme_{name}"
-
- elif cmd.startswith("constant"):
- found = False
-
- # Search in the current class
- search_class_defs = [class_def]
-
- if link_target.find(".") == -1:
- # Also search in @GlobalScope as a last resort if no class was specified
- search_class_defs.append(state.classes["@GlobalScope"])
-
- for search_class_def in search_class_defs:
- if method_param in search_class_def.constants:
- class_param = search_class_def.name
- found = True
-
- else:
- for enum in search_class_def.enums.values():
- if method_param in enum.values:
- class_param = search_class_def.name
- found = True
- break
-
- if not found:
- print_error(
- f'{state.current_class}.xml: Unresolved constant reference "{link_target}" in {context_name}.',
- state,
- )
-
- else:
- print_error(
- f'{state.current_class}.xml: Unresolved type reference "{class_param}" in method reference "{link_target}" in {context_name}.',
- state,
- )
-
- repl_text = method_param
- if class_param != state.current_class:
- repl_text = f"{class_param}.{method_param}"
- tag_text = f":ref:`{repl_text}`"
- escape_pre = True
- escape_post = True
-
- elif cmd.startswith("enum"):
- tag_text = make_enum(link_target, False, state)
- escape_pre = True
- escape_post = True
-
- elif cmd.startswith("param"):
- valid_context = isinstance(context, (MethodDef, SignalDef, AnnotationDef))
- if not valid_context:
- print_error(
- f'{state.current_class}.xml: Argument reference "{link_target}" used outside of method, signal, or annotation context in {context_name}.',
- state,
- )
- else:
- context_params: List[ParameterDef] = context.parameters # type: ignore
- found = False
- for param_def in context_params:
- if param_def.name == link_target:
- found = True
- break
- if not found:
- print_error(
- f'{state.current_class}.xml: Unresolved argument reference "{link_target}" in {context_name}.',
- state,
- )
-
- tag_text = f"``{link_target}``"
- escape_pre = True
- escape_post = True
-
- # Formatting directives.
-
- elif is_in_tagset(cmd, ["url"]):
- if cmd.startswith("url="):
- # 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(
- f"{state.current_class}.xml: Tag depth mismatch for [url]: no closing [/url] in {context_name}.",
- 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)
- continue
- else:
- print_error(
- f'{state.current_class}.xml: Misformatted [url] tag "{cmd}" in {context_name}.',
- state,
- )
-
- elif cmd == "br":
- # Make a new paragraph instead of a linebreak, rst is not so linebreak friendly
- tag_text = "\n\n"
- # Strip potential leading spaces
- while post_text[0] == " ":
- post_text = post_text[1:]
-
- elif cmd == "center" or cmd == "/center":
- if cmd == "/center":
- tag_depth -= 1
- else:
- tag_depth += 1
- tag_text = ""
-
- 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 == "kbd" or cmd == "/kbd":
- tag_text = "`"
- if cmd == "/kbd":
- tag_depth -= 1
- escape_post = True
- else:
- tag_text = ":kbd:" + tag_text
- tag_depth += 1
- escape_pre = True
-
- # Invalid syntax checks.
- elif cmd.startswith("/"):
- print_error(f'{state.current_class}.xml: Unrecognized closing tag "{cmd}" in {context_name}.', state)
-
- tag_text = f"[{tag_text}]"
-
- else:
- print_error(f'{state.current_class}.xml: Unrecognized opening tag "{cmd}" in {context_name}.', state)
-
- tag_text = f"``{tag_text}``"
- escape_pre = True
- escape_post = True
-
- # Properly escape things like `[Node]s`
- 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)
- iter_pos = 0
- while not inside_code:
- iter_pos = post_text.find("*", iter_pos, next_brac_pos)
- if iter_pos == -1:
- break
- post_text = f"{post_text[:iter_pos]}\*{post_text[iter_pos + 1 :]}"
- iter_pos += 2
-
- iter_pos = 0
- while not inside_code:
- iter_pos = post_text.find("_", iter_pos, next_brac_pos)
- if iter_pos == -1:
- break
- if not post_text[iter_pos + 1].isalnum(): # don't escape within a snake_case word
- post_text = f"{post_text[:iter_pos]}\_{post_text[iter_pos + 1 :]}"
- iter_pos += 2
- else:
- iter_pos += 1
-
- text = pre_text + tag_text + post_text
- pos = len(pre_text) + len(tag_text)
-
- if tag_depth > 0:
- print_error(
- f"{state.current_class}.xml: Tag depth mismatch: too many (or too few) open/close tags in {context_name}.",
- state,
- )
-
- return text
-
-
-def format_context_name(context: Union[DefinitionBase, None]) -> str:
- context_name: str = "unknown context"
- if context is not None:
- context_name = f'{context.definition_name} "{context.name}" description'
-
- return context_name
-
-
-def escape_rst(text: str, until_pos: int = -1) -> 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 = f"{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 = f"{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 = f"{text[:pos]}\_{text[pos + 1 :]}"
- pos += 2
- else:
- pos += 1
-
- return text
-
-
-def format_codeblock(code_type: str, post_text: str, indent_level: int, state: State) -> Union[Tuple[str, int], None]:
- end_pos = post_text.find("[/" + code_type + "]")
- if end_pos == -1:
- print_error(f"{state.current_class}.xml: [{code_type}] without a closing tag.", state)
- return None
-
- code_text = post_text[len(f"[{code_type}]") : end_pos]
- post_text = post_text[end_pos:]
-
- # Remove extraneous tabs
- code_pos = 0
- while True:
- code_pos = code_text.find("\n", code_pos)
- if code_pos == -1:
- break
-
- to_skip = 0
- 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(
- f"{state.current_class}.xml: Four spaces should be used for indentation within [{code_type}].",
- state,
- )
-
- if len(code_text[code_pos + to_skip + 1 :]) == 0:
- code_text = f"{code_text[:code_pos]}\n"
- code_pos += 1
- else:
- code_text = f"{code_text[:code_pos]}\n {code_text[code_pos + to_skip + 1 :]}"
- code_pos += 5 - to_skip
- return (f"\n[{code_type}]{code_text}{post_text}", len(f"\n[{code_type}]{code_text}"))
-
-
-def format_table(f: TextIO, data: List[Tuple[Optional[str], ...]], remove_empty_columns: bool = False) -> None:
- if len(data) == 0:
- return
-
- f.write(".. table::\n")
- f.write(" :widths: auto\n\n")
-
- # Calculate the width of each column first, we will use this information
- # to properly format RST-style tables.
- 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
-
- # Each table row is wrapped in two separators, consecutive rows share the same separator.
- # All separators, or rather borders, have the same shape and content. We compose it once,
- # then reuse it.
-
- sep = ""
- for size in column_sizes:
- if size == 0 and remove_empty_columns:
- continue
- sep += "+" + "-" * (size + 2) # Content of each cell is padded by 1 on each side.
- sep += "+\n"
-
- # Draw the first separator.
- f.write(f" {sep}")
-
- # Draw each row and close it with a separator.
- for row in data:
- row_text = "|"
- for i, text in enumerate(row):
- if column_sizes[i] == 0 and remove_empty_columns:
- continue
- row_text += f' {(text or "").ljust(column_sizes[i])} |'
- row_text += "\n"
-
- f.write(f" {row_text}")
- f.write(f" {sep}")
-
- f.write("\n")
-
-
-def sanitize_operator_name(dirty_name: str, state: 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 == "**":
- clear_name = "pow"
-
- 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(f'Unsupported operator type "{dirty_name}", please add the missing rule.', state)
-
- return clear_name
-
-
-if __name__ == "__main__":
- main()
diff --git a/docs/api/tools/version.py b/docs/api/tools/version.py
deleted file mode 100644
index 066d5e1c2..000000000
--- a/docs/api/tools/version.py
+++ /dev/null
@@ -1 +0,0 @@
-docs = "latest"
diff --git a/docs/assets/icon.svg b/docs/assets/icon.svg
new file mode 100644
index 000000000..70b92722f
--- /dev/null
+++ b/docs/assets/icon.svg
@@ -0,0 +1,133 @@
+
+
+
+
diff --git a/docs/media/makefile.png b/docs/assets/makefile.png
similarity index 100%
rename from docs/media/makefile.png
rename to docs/assets/makefile.png
diff --git a/docs/assets/scene-tree.png b/docs/assets/scene-tree.png
new file mode 100644
index 000000000..71373a757
Binary files /dev/null and b/docs/assets/scene-tree.png differ
diff --git a/docs/media/screenshot01.png b/docs/assets/screenshot01.png
similarity index 100%
rename from docs/media/screenshot01.png
rename to docs/assets/screenshot01.png
diff --git a/docs/media/screenshot02.png b/docs/assets/screenshot02.png
similarity index 100%
rename from docs/media/screenshot02.png
rename to docs/assets/screenshot02.png
diff --git a/docs/media/screenshot03.png b/docs/assets/screenshot03.png
similarity index 100%
rename from docs/media/screenshot03.png
rename to docs/assets/screenshot03.png
diff --git a/docs/media/screenshot04.png b/docs/assets/screenshot04.png
similarity index 100%
rename from docs/media/screenshot04.png
rename to docs/assets/screenshot04.png
diff --git a/docs/class-reference/.nav.yml b/docs/class-reference/.nav.yml
new file mode 100644
index 000000000..3c00279ba
--- /dev/null
+++ b/docs/class-reference/.nav.yml
@@ -0,0 +1,234 @@
+nav:
+ - ./APUDatabase.md
+ - ./APUEntry.md
+ - ./ActionStartInputPlumber.md
+ - ./ActionStartPowerStation.md
+ - ./ActionTurboTakeover.md
+ - ./AppLifecycleHook.md
+ - ./AudioManager.md
+ - ./BackInputHandler.md
+ - ./BehaviorNode.md
+ - ./Bitwise.md
+ - ./BlockDevice.md
+ - ./BluetoothAdapter.md
+ - ./BluetoothDevice.md
+ - ./BluetoothManager.md
+ - ./BluezInstance.md
+ - ./BoxArtManager.md
+ - ./BoxArtProvider.md
+ - ./CPU.md
+ - ./CPUCore.md
+ - ./Cache.md
+ - ./CardButton.md
+ - ./CardButtonSetting.md
+ - ./CardIconButton.md
+ - ./CardInputIconButton.md
+ - ./CardMappingButton.md
+ - ./CardMappingButtonGroup.md
+ - ./Command.md
+ - ./ComponentTextInput.md
+ - ./CompositeDevice.md
+ - ./Cpu.md
+ - ./CpuCore.md
+ - ./CustomLogger.md
+ - ./DBusDevice.md
+ - ./DRMCardInfo.md
+ - ./DRMCardInfoAMD.md
+ - ./DRMCardInfoIntel.md
+ - ./DRMCardPort.md
+ - ./Dialog.md
+ - ./DiskManager.md
+ - ./DisplayManager.md
+ - ./DisplayManager.BacklightProvider.md
+ - ./DisplayManager.BrightnessctlBacklight.md
+ - ./DisplayManager.SteamOsBacklight.md
+ - ./DriveCard.md
+ - ./DriveDevice.md
+ - ./Dropdown.md
+ - ./Effect.md
+ - ./EnhancedScrollContainer.md
+ - ./EventDevice.md
+ - ./ExpandableCard.md
+ - ./FadeEffect.md
+ - ./FifoReader.md
+ - ./FilesystemDevice.md
+ - ./FocusGroup.md
+ - ./FocusGroupSetter.md
+ - ./FocusManager.md
+ - ./FocusSetter.md
+ - ./FocusStack.md
+ - ./GameCard.md
+ - ./GamepadDevice.md
+ - ./GamepadMapper.md
+ - ./Gamescope.md
+ - ./GamescopeInstance.md
+ - ./GamescopeXWayland.md
+ - ./GitHubClient.md
+ - ./Gpu.md
+ - ./GpuCard.md
+ - ./GpuConnector.md
+ - ./GrowerEffect.md
+ - ./GutHookScript.md
+ - ./GutStringUtils.md
+ - ./GutTest.md
+ - ./GutUtils.md
+ - ./HTTPAPIClient.md
+ - ./HTTPAPIClient.Response.md
+ - ./HTTPImageFetcher.md
+ - ./HandheldIconMapping.md
+ - ./HandheldPlatform.md
+ - ./HardwareManager.md
+ - ./HardwareManager.GPUInfo.md
+ - ./InputIcon.md
+ - ./InputIconKeyboardMapping.md
+ - ./InputIconManager.md
+ - ./InputIconMapping.md
+ - ./InputIconProcessor.md
+ - ./InputManager.md
+ - ./InputPlumber.md
+ - ./InputPlumberAxisEvent.md
+ - ./InputPlumberEvent.md
+ - ./InputPlumberGamepadEvent.md
+ - ./InputPlumberGyroEvent.md
+ - ./InputPlumberInstance.md
+ - ./InputPlumberMapping.md
+ - ./InputPlumberMouseEvent.md
+ - ./InputPlumberMouseMotionEvent.md
+ - ./InputPlumberProfile.md
+ - ./InputPlumberTouchEvent.md
+ - ./InputPlumberTouchMotionEvent.md
+ - ./InputPlumberTouchpadEvent.md
+ - ./InputPlumberTriggerEvent.md
+ - ./InputWatcher.md
+ - ./InstallLocationCard.md
+ - ./InstallLocationDialog.md
+ - ./InstallManager.md
+ - ./InstallManager.Request.md
+ - ./InstallOptionDialog.md
+ - ./InteractiveProcess.md
+ - ./KeyboardContext.md
+ - ./KeyboardDevice.md
+ - ./KeyboardInstance.md
+ - ./KeyboardKeyConfig.md
+ - ./KeyboardLayout.md
+ - ./KeyboardOpener.md
+ - ./KeyboardRow.md
+ - ./LaunchManager.md
+ - ./Launcher.md
+ - ./LevelIndicator.md
+ - ./Library.md
+ - ./Library.InstallLocation.md
+ - ./Library.InstallOption.md
+ - ./LibraryDeck.md
+ - ./LibraryItem.md
+ - ./LibraryLaunchItem.md
+ - ./LibraryManager.md
+ - ./LibraryRefreshState.md
+ - ./LibraryRefresher.md
+ - ./Log.md
+ - ./LogManager.md
+ - ./MangoApp.md
+ - ./MouseDevice.md
+ - ./MultiHTTPRequest.md
+ - ./NetworkAccessPoint.md
+ - ./NetworkActiveConnection.md
+ - ./NetworkDevice.md
+ - ./NetworkDeviceWireless.md
+ - ./NetworkIpv4Config.md
+ - ./NetworkManager.md
+ - ./NetworkManagerInstance.md
+ - ./NodeThread.md
+ - ./Notification.md
+ - ./NotificationContainer.md
+ - ./NotificationManager.md
+ - ./OSPlatform.md
+ - ./OnScreenKeyboard.md
+ - ./OverlayContainer.md
+ - ./OverlayInputManager.md
+ - ./OverlayProvider.md
+ - ./PackageVerifier.md
+ - ./PartitionCard.md
+ - ./PartitionDevice.md
+ - ./PerformanceManager.md
+ - ./PerformanceProfile.md
+ - ./PipeManager.md
+ - ./Platform.md
+ - ./Platform.OSInfo.md
+ - ./PlatformAction.md
+ - ./PlatformBazzite.md
+ - ./PlatformChimeraOS.md
+ - ./PlatformManjaro.md
+ - ./PlatformNixOS.md
+ - ./PlatformProvider.md
+ - ./PlatformSteamOS.md
+ - ./PlayAudioEffect.md
+ - ./Plugin.md
+ - ./PluginLoader.md
+ - ./PluginManager.md
+ - ./PowerManager.md
+ - ./PowerSaver.md
+ - ./PowerStation.md
+ - ./PowerStationInstance.md
+ - ./ProgressDialog.md
+ - ./Pty.md
+ - ./QuickBarCard.md
+ - ./RaiseEffect.md
+ - ./Reaper.md
+ - ./ResourceProcessor.md
+ - ./ResourceRegistry.md
+ - ./RunningApp.md
+ - ./Sandbox.md
+ - ./SandboxBubblewrap.md
+ - ./SandboxFirejail.md
+ - ./ScrollerJoystick.md
+ - ./SearchBar.md
+ - ./SelectableText.md
+ - ./SemanticVersion.md
+ - ./SettingsManager.md
+ - ./SharedThread.md
+ - ./SharedThread.ExecutingTask.md
+ - ./SharedThread.ScheduledTask.md
+ - ./SlideEffect.md
+ - ./SmoothScrollEffect.md
+ - ./SoftwareUpdater.md
+ - ./State.md
+ - ./StateChanger.md
+ - ./StateMachine.md
+ - ./StateMachineWatcher.md
+ - ./StateManager.md
+ - ./StateUpdater.md
+ - ./StateWatcher.md
+ - ./StatesWatcher.md
+ - ./StatusPanel.md
+ - ./SteamRemovableMediaManager.md
+ - ./Store.md
+ - ./StoreItem.md
+ - ./StoreItemDetails.md
+ - ./StoreManager.md
+ - ./SubReaper.md
+ - ./TabContainerState.md
+ - ./TabLabel.md
+ - ./TabSetter.md
+ - ./TabsHeader.md
+ - ./TextSetter.md
+ - ./ThemeSetter.md
+ - ./ThemeUtils.md
+ - ./ThreadGroup.md
+ - ./ThreadPool.md
+ - ./ThreadPool.Task.md
+ - ./Toggle.md
+ - ./Transition.md
+ - ./TransitionContainer.md
+ - ./UDisks2Instance.md
+ - ./UPowerDevice.md
+ - ./UPowerInstance.md
+ - ./UUID.md
+ - ./UserInterface.md
+ - ./ValueSlider.md
+ - ./Vdf.md
+ - ./Version.md
+ - ./VisibilityManager.md
+ - ./WatchdogThread.md
+ - ./WebsocketRPCClient.md
+ - ./WifiNetworkTree.md
+ - ./Xdg.md
diff --git a/docs/class-reference/APUDatabase.md b/docs/class-reference/APUDatabase.md
new file mode 100644
index 000000000..7d16537c9
--- /dev/null
+++ b/docs/class-reference/APUDatabase.md
@@ -0,0 +1,93 @@
+# APUDatabase
+
+**Inherits:** [Resource](https://docs.godotengine.org/en/stable/classes/class_resource.html)
+
+
+## Properties
+
+| Type | Name | Default |
+| ---- | ---- | ------- |
+| [APUEntry[]](../APUEntry) | [apu_list](./#apu_list) | |
+| [String](https://docs.godotengine.org/en/stable/classes/class_string.html) | [database_name](./#database_name) | |
+| [Dictionary](https://docs.godotengine.org/en/stable/classes/class_dictionary.html) | [apu_map](./#apu_map) | |
+| [bool](https://docs.godotengine.org/en/stable/classes/class_bool.html) | [loaded](./#loaded) | false |
+| [CustomLogger](../CustomLogger) | [logger](./#logger) | |
+
+## Methods
+
+| Returns | Signature |
+| ------- | --------- |
+| void | [load_db](./#load_db)() |
+| [APUEntry](../APUEntry) | [get_apu](./#get_apu)(apu_name: [String](https://docs.godotengine.org/en/stable/classes/class_string.html)) |
+
+
+------------------
+
+## Property Descriptions
+
+### `apu_list`
+
+
+[APUEntry[]](../APUEntry) apu_list
+
+
+!!! note
+ There is currently no description for this property. Please help us by contributing one!
+
+### `database_name`
+
+
+[String](https://docs.godotengine.org/en/stable/classes/class_string.html) database_name
+
+
+!!! note
+ There is currently no description for this property. Please help us by contributing one!
+
+### `apu_map`
+
+
+[Dictionary](https://docs.godotengine.org/en/stable/classes/class_dictionary.html) apu_map
+
+
+!!! note
+ There is currently no description for this property. Please help us by contributing one!
+
+### `loaded`
+
+
+[bool](https://docs.godotengine.org/en/stable/classes/class_bool.html) loaded = false
+
+
+!!! note
+ There is currently no description for this property. Please help us by contributing one!
+
+### `logger`
+
+
+[CustomLogger](../CustomLogger) logger
+
+
+!!! note
+ There is currently no description for this property. Please help us by contributing one!
+
+
+
+
+------------------
+
+## Method Descriptions
+
+### `load_db()`
+
+
+void **load_db**()
+
+
+Load entries that are set in the APUDatabase resource file into a map. NOTE: This needs to be called after _init() in order for the exported apu_list to be populated.
+### `get_apu()`
+
+
+[APUEntry](../APUEntry) **get_apu**(apu_name: [String](https://docs.godotengine.org/en/stable/classes/class_string.html))
+
+
+Returns an [APUEntry](../APUEntry) of the given APU
diff --git a/docs/class-reference/APUEntry.md b/docs/class-reference/APUEntry.md
new file mode 100644
index 000000000..1faf63b54
--- /dev/null
+++ b/docs/class-reference/APUEntry.md
@@ -0,0 +1,57 @@
+# APUEntry
+
+**Inherits:** [Resource](https://docs.godotengine.org/en/stable/classes/class_resource.html)
+
+
+## Properties
+
+| Type | Name | Default |
+| ---- | ---- | ------- |
+| [String](https://docs.godotengine.org/en/stable/classes/class_string.html) | [model_name](./#model_name) | |
+| [float](https://docs.godotengine.org/en/stable/classes/class_float.html) | [min_tdp](./#min_tdp) | |
+| [float](https://docs.godotengine.org/en/stable/classes/class_float.html) | [max_tdp](./#max_tdp) | |
+| [float](https://docs.godotengine.org/en/stable/classes/class_float.html) | [max_boost](./#max_boost) | |
+
+
+
+------------------
+
+## Property Descriptions
+
+### `model_name`
+
+
+[String](https://docs.godotengine.org/en/stable/classes/class_string.html) model_name
+
+
+!!! note
+ There is currently no description for this property. Please help us by contributing one!
+
+### `min_tdp`
+
+
+[float](https://docs.godotengine.org/en/stable/classes/class_float.html) min_tdp
+
+
+!!! note
+ There is currently no description for this property. Please help us by contributing one!
+
+### `max_tdp`
+
+
+[float](https://docs.godotengine.org/en/stable/classes/class_float.html) max_tdp
+
+
+!!! note
+ There is currently no description for this property. Please help us by contributing one!
+
+### `max_boost`
+
+
+[float](https://docs.godotengine.org/en/stable/classes/class_float.html) max_boost
+
+
+!!! note
+ There is currently no description for this property. Please help us by contributing one!
+
+
diff --git a/docs/class-reference/ActionStartInputPlumber.md b/docs/class-reference/ActionStartInputPlumber.md
new file mode 100644
index 000000000..248ef1713
--- /dev/null
+++ b/docs/class-reference/ActionStartInputPlumber.md
@@ -0,0 +1,25 @@
+# ActionStartInputPlumber
+
+**Inherits:** [PlatformAction](../PlatformAction)
+
+
+## Methods
+
+| Returns | Signature |
+| ------- | --------- |
+| void | [execute](./#execute)() |
+
+
+------------------
+
+## Method Descriptions
+
+### `execute()`
+
+
+void **execute**()
+
+
+!!! note
+ There is currently no description for this method. Please help us by contributing one!
+
diff --git a/docs/class-reference/ActionStartPowerStation.md b/docs/class-reference/ActionStartPowerStation.md
new file mode 100644
index 000000000..374505d94
--- /dev/null
+++ b/docs/class-reference/ActionStartPowerStation.md
@@ -0,0 +1,25 @@
+# ActionStartPowerStation
+
+**Inherits:** [PlatformAction](../PlatformAction)
+
+
+## Methods
+
+| Returns | Signature |
+| ------- | --------- |
+| void | [execute](./#execute)() |
+
+
+------------------
+
+## Method Descriptions
+
+### `execute()`
+
+
+void **execute**()
+
+
+!!! note
+ There is currently no description for this method. Please help us by contributing one!
+
diff --git a/docs/class-reference/ActionTurboTakeover.md b/docs/class-reference/ActionTurboTakeover.md
new file mode 100644
index 000000000..5c599b232
--- /dev/null
+++ b/docs/class-reference/ActionTurboTakeover.md
@@ -0,0 +1,25 @@
+# ActionTurboTakeover
+
+**Inherits:** [PlatformAction](../PlatformAction)
+
+
+## Methods
+
+| Returns | Signature |
+| ------- | --------- |
+| void | [execute](./#execute)() |
+
+
+------------------
+
+## Method Descriptions
+
+### `execute()`
+
+
+void **execute**()
+
+
+!!! note
+ There is currently no description for this method. Please help us by contributing one!
+
diff --git a/docs/class-reference/AppLifecycleHook.md b/docs/class-reference/AppLifecycleHook.md
new file mode 100644
index 000000000..c9b85b627
--- /dev/null
+++ b/docs/class-reference/AppLifecycleHook.md
@@ -0,0 +1,42 @@
+# AppLifecycleHook
+
+**Inherits:** [RefCounted](https://docs.godotengine.org/en/stable/classes/class_refcounted.html)
+
+Base class for executing callbacks at certain points of an app's lifecycle.
+## Description
+
+This class provides an interface for executing arbitrary callbacks at certain points of an application's lifecycle. This can allow [Library](../Library) implementations the ability to execute actions when apps are about to start, have started, or have exited.
+## Methods
+
+| Returns | Signature |
+| ------- | --------- |
+| [String](https://docs.godotengine.org/en/stable/classes/class_string.html) | [get_name](./#get_name)() |
+| void | [execute](./#execute)(item: [LibraryLaunchItem](../LibraryLaunchItem)) |
+| [int](https://docs.godotengine.org/en/stable/classes/class_int.html) | [get_type](./#get_type)() |
+
+
+------------------
+
+## Method Descriptions
+
+### `get_name()`
+
+
+[String](https://docs.godotengine.org/en/stable/classes/class_string.html) **get_name**()
+
+
+Name of the lifecycle hook
+### `execute()`
+
+
+void **execute**(item: [LibraryLaunchItem](../LibraryLaunchItem))
+
+
+Executes whenever an app from this library reaches the stage in its lifecycle designated by the hook type. E.g. a `PRE_LAUNCH` hook will have this method called whenever an app is about to launch.
+### `get_type()`
+
+
+[int](https://docs.godotengine.org/en/stable/classes/class_int.html) **get_type**()
+
+
+Returns the hook type, which designates where in the application's lifecycle the hook should be executed.
diff --git a/docs/class-reference/AudioManager.md b/docs/class-reference/AudioManager.md
new file mode 100644
index 000000000..b6e0bf093
--- /dev/null
+++ b/docs/class-reference/AudioManager.md
@@ -0,0 +1,106 @@
+# AudioManager
+
+**Inherits:** [Resource](https://docs.godotengine.org/en/stable/classes/class_resource.html)
+
+Manage system volume and audio devices
+## Description
+
+The AudioManager is responsible for managing the system volume and audio devices if the host supports it.
+## Properties
+
+| Type | Name | Default |
+| ---- | ---- | ------- |
+| [float](https://docs.godotengine.org/en/stable/classes/class_float.html) | [current_volume](./#current_volume) | |
+
+## Methods
+
+| Returns | Signature |
+| ------- | --------- |
+| [bool](https://docs.godotengine.org/en/stable/classes/class_bool.html) | [supports_audio](./#supports_audio)() |
+| [int](https://docs.godotengine.org/en/stable/classes/class_int.html) | [set_volume](./#set_volume)(value: [float](https://docs.godotengine.org/en/stable/classes/class_float.html), type: [int](https://docs.godotengine.org/en/stable/classes/class_int.html) = 0) |
+| [int](https://docs.godotengine.org/en/stable/classes/class_int.html) | [toggle_mute](./#toggle_mute)() |
+| [int](https://docs.godotengine.org/en/stable/classes/class_int.html) | [set_output_device](./#set_output_device)(device: [String](https://docs.godotengine.org/en/stable/classes/class_string.html)) |
+| [String](https://docs.godotengine.org/en/stable/classes/class_string.html) | [get_current_output_device](./#get_current_output_device)() |
+| [float](https://docs.godotengine.org/en/stable/classes/class_float.html) | [get_current_volume](./#get_current_volume)() |
+| [PackedStringArray](https://docs.godotengine.org/en/stable/classes/class_packedstringarray.html) | [get_output_devices](./#get_output_devices)() |
+
+
+------------------
+
+## Property Descriptions
+
+### `current_volume`
+
+
+[float](https://docs.godotengine.org/en/stable/classes/class_float.html) current_volume
+
+
+Current volume
+
+
+
+------------------
+
+## Method Descriptions
+
+### `supports_audio()`
+
+
+[bool](https://docs.godotengine.org/en/stable/classes/class_bool.html) **supports_audio**()
+
+
+Returns true if the system has audio controls we support
+### `set_volume()`
+
+
+[int](https://docs.godotengine.org/en/stable/classes/class_int.html) **set_volume**(value: [float](https://docs.godotengine.org/en/stable/classes/class_float.html), type: [int](https://docs.godotengine.org/en/stable/classes/class_int.html) = 0)
+
+
+Sets the current audio device volume based on the given value. The volume value should be in the form of a percent where 1.0 equals 100%. The type can be either absolute (default) or relative volume values.
+
+
+```gdscript
+
+ const AudioManager := preload("res://core/global/audio_manager.tres")
+ ...
+ AudioManager.set_volume(1.0) # Set volume to 100%
+ AudioManager.set_volume(-0.06, AudioManager.TYPE.RELATIVE) # Decrease volume by 6%
+
+```
+
+
+### `toggle_mute()`
+
+
+[int](https://docs.godotengine.org/en/stable/classes/class_int.html) **toggle_mute**()
+
+
+Toggles mute on the current audio device
+### `set_output_device()`
+
+
+[int](https://docs.godotengine.org/en/stable/classes/class_int.html) **set_output_device**(device: [String](https://docs.godotengine.org/en/stable/classes/class_string.html))
+
+
+Sets the current output device to the given device
+### `get_current_output_device()`
+
+
+[String](https://docs.godotengine.org/en/stable/classes/class_string.html) **get_current_output_device**()
+
+
+Returns the currently set output device
+### `get_current_volume()`
+
+
+[float](https://docs.godotengine.org/en/stable/classes/class_float.html) **get_current_volume**()
+
+
+Returns the current volume as a percentage. E.g. 0.52 is 52%
+### `get_output_devices()`
+
+
+[PackedStringArray](https://docs.godotengine.org/en/stable/classes/class_packedstringarray.html) **get_output_devices**()
+
+
+Returns a list of audio output devices
diff --git a/docs/class-reference/BackInputHandler.md b/docs/class-reference/BackInputHandler.md
new file mode 100644
index 000000000..44746ae7f
--- /dev/null
+++ b/docs/class-reference/BackInputHandler.md
@@ -0,0 +1,49 @@
+# BackInputHandler
+
+**Inherits:** [Node](https://docs.godotengine.org/en/stable/classes/class_node.html)
+
+DEPRECATED: Use [InputWatcher](../InputWatcher) with [StateUpdater](../StateUpdater) instead
+## Properties
+
+| Type | Name | Default |
+| ---- | ---- | ------- |
+| [StateMachine](../StateMachine) | [state_machine](./#state_machine) |