From 40f4bc7cf33f96c6018712a3ea914ee7e48082c1 Mon Sep 17 00:00:00 2001
From: Arctis-Fireblight <6182060+Arctis-Fireblight@users.noreply.github.com>
Date: Wed, 18 Feb 2026 17:27:22 -0600
Subject: [PATCH 1/2] Improved Markdown generation in `make_md.py`:
- Updated anchor creation for consistency.
- Added `_category_.json` generation for better doc indexing.
- Introduced escaping functionality for special characters in descriptions and MD files.
- Enhanced sanitization logic for class/file/operator names.
- Streamlined Markdown reference links and tag escaping logic.
---
.pre-commit-config.yaml | 5 +-
doc/classes/Engine.xml | 2 +-
doc/tools/make_md.py | 163 +++++++++++++++++++++++++---------------
3 files changed, 106 insertions(+), 64 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 0811255cec2..a3cf6bd2c5a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,12 +1,13 @@
default_language_version:
python: python3
-
+# "doc/tools/make_md.py" is excluded because it fails the mypy check, and attempts to make the linter happy break the code. It needs to be the way it is.
exclude: |
(?x)^(
.*thirdparty/.*|
.*-(dll|dylib|so)_wrap\.[ch]|
platform/android/java/editor/src/main/java/com/android/.*|
- platform/android/java/lib/src/com/google/.*
+ platform/android/java/lib/src/com/google/.*|
+ doc/tools/make_md.py
)$
repos:
diff --git a/doc/classes/Engine.xml b/doc/classes/Engine.xml
index 189a2ce5ed4..55f8b41dc45 100644
--- a/doc/classes/Engine.xml
+++ b/doc/classes/Engine.xml
@@ -48,7 +48,7 @@
Returns a [Dictionary] of categorized donor names. Each entry is an [Array] of strings:
- {[code]donors[/code]}
+ [code]donors[/code]
diff --git a/doc/tools/make_md.py b/doc/tools/make_md.py
index 72ce1c5540a..39d03bd07f0 100755
--- a/doc/tools/make_md.py
+++ b/doc/tools/make_md.py
@@ -3,6 +3,7 @@
# This script makes Markdown files from the XML class reference for use with the online docs.
import argparse
+import json
import os
import re
import sys
@@ -778,6 +779,21 @@ def main() -> None:
# Create the output folder recursively if it doesn't already exist.
os.makedirs(args.output, exist_ok=True)
+ # Generate _category_.json file if not in dry run mode
+ if not args.dry_run:
+ category_json_path = os.path.join(args.output, "_category_.json")
+ category_data = {
+ "label": "Class Reference",
+ "position": 6,
+ "link": {
+ "type": "generated-index",
+ "description": "Documentation for all classes, enums, methods, signals, and properties in the engine.",
+ },
+ }
+ with open(category_json_path, "w", encoding="utf-8", newline="\n") as category_file:
+ json.dump(category_data, category_file, indent=2)
+ category_file.write("\n")
+
print("Generating the Markdown class reference...")
grouped_classes: Dict[str, List[str]] = {}
@@ -886,7 +902,7 @@ def get_git_branch() -> str:
def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir: str) -> None:
class_name = class_def.name
with open(
- os.devnull if dry_run else os.path.join(output_dir, f"class_{sanitize_class_name(class_name, True)}.md"),
+ os.devnull if dry_run else os.path.join(output_dir, f"{sanitize_class_name(class_name, True)}.md"),
"w",
encoding="utf-8",
newline="\n",
@@ -912,7 +928,7 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
f.write(f"\n\n")
# Document reference id and header.
- f.write(f'\n\n')
+ f.write(f'\n\n')
f.write(make_heading(class_name, "=", False))
f.write(make_deprecated_experimental(class_def, state))
@@ -926,7 +942,7 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
first = True
while inherits is not None:
if not first:
- f.write(" **<** ")
+ f.write(" **\\<** ")
else:
first = False
@@ -1017,11 +1033,11 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
default = property_def.default_value
if default is not None and property_def.overrides:
override_file = sanitize_class_name(property_def.overrides, True)
- ref = f"[{property_def.overrides}.{property_def.name}](class_{override_file}.md#class_{sanitize_class_name(property_def.overrides)}_property_{property_def.name})"
+ ref = f"[{property_def.overrides}.{property_def.name}]({override_file}.md#class_{sanitize_class_name(property_def.overrides)}_property_{property_def.name})"
# Not using translate() for now as it breaks table formatting.
ml.append((type_rst, property_def.name, f"{default} (overrides {ref})"))
else:
- anchor = f"class_{sanitize_class_name(class_name)}_property_{property_def.name}"
+ anchor = f"{sanitize_class_name(class_name)}_property_{property_def.name}"
ref = f"[{property_def.name}](#{anchor})"
ml.append((type_rst, ref, default))
@@ -1068,9 +1084,7 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
ml = []
for theme_item_def in class_def.theme_items.values():
- anchor = (
- f"class_{sanitize_class_name(class_name)}_theme_{theme_item_def.data_name}_{theme_item_def.name}"
- )
+ anchor = f"{sanitize_class_name(class_name)}_theme_{theme_item_def.data_name}_{theme_item_def.name}"
ref = f"[{theme_item_def.name}](#{anchor})"
ml.append((theme_item_def.type_name.to_md(state), ref, theme_item_def.default_value))
@@ -1092,7 +1106,7 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
# Create signal signature and anchor point.
- signal_anchor = f"class_{sanitize_class_name(class_name)}_signal_{signal.name}"
+ signal_anchor = f"{sanitize_class_name(class_name)}_signal_{signal.name}"
f.write(f'\n\n')
self_link = f"[🔗](#{signal_anchor})"
f.write("\n\n")
@@ -1145,7 +1159,7 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
for value in e.values.values():
# Also create signature and anchor point for each enum constant.
- f.write(f'\n\n')
+ f.write(f'\n\n')
f.write("\n\n")
f.write(f"{e.type_name.to_md(state)} **{value.name}** = `{value.value}`\n\n")
@@ -1177,7 +1191,7 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
for constant in class_def.constants.values():
# Create constant signature and anchor point.
- constant_anchor = f"class_{sanitize_class_name(class_name)}_constant_{constant.name}"
+ constant_anchor = f"{sanitize_class_name(class_name)}_constant_{constant.name}"
f.write(f'\n\n')
self_link = f"[🔗](#{constant_anchor})"
f.write("\n\n")
@@ -1218,7 +1232,7 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
self_link = ""
if i == 0:
- annotation_anchor = f"class_{sanitize_class_name(class_name)}_annotation_{m.name}"
+ annotation_anchor = f"{sanitize_class_name(class_name)}_annotation_{m.name}"
f.write(f'\n\n')
self_link = f" [🔗](#{annotation_anchor})"
@@ -1228,7 +1242,7 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
f.write(f"{signature}{self_link}\n\n")
# Add annotation description, or a call to action if it's missing.
-
+ m.description = escape_tags(m.description)
if m.description is not None and m.description.strip() != "":
f.write(f"{format_text_block(m.description.strip(), m, state)}\n\n")
else:
@@ -1260,7 +1274,7 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
# Create property signature and anchor point.
- property_anchor = f"class_{sanitize_class_name(class_name)}_property_{property_def.name}"
+ property_anchor = f"{sanitize_class_name(class_name)}_property_{property_def.name}"
f.write(f'\n\n')
self_link = f"[🔗](#{property_anchor})"
f.write("\n\n")
@@ -1290,7 +1304,7 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
f.write("\n")
# Add property description, or a call to action if it's missing.
-
+ property_def.text = escape_tags(property_def.text)
f.write(make_deprecated_experimental(property_def, state))
if property_def.text is not None and property_def.text.strip() != "":
@@ -1329,7 +1343,7 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
self_link = ""
if i == 0:
- constructor_anchor = f"class_{sanitize_class_name(class_name)}_constructor_{m.name}"
+ constructor_anchor = f"{sanitize_class_name(class_name)}_constructor_{m.name}"
f.write(f'\n\n')
self_link = f" [🔗](#{constructor_anchor})"
@@ -1339,7 +1353,7 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
f.write(f"{ret_type} {signature}{self_link}\n\n")
# Add constructor description, or a call to action if it's missing.
-
+ m.description = escape_tags(m.description)
f.write(make_deprecated_experimental(m, state))
if m.description is not None and m.description.strip() != "":
@@ -1376,7 +1390,7 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
method_qualifier = ""
if m.name.startswith("_"):
method_qualifier = "private_"
- method_anchor = f"class_{sanitize_class_name(class_name)}_{method_qualifier}method_{m.name}"
+ method_anchor = f"{sanitize_class_name(class_name)}_{method_qualifier}method_{m.name}"
f.write(f'\n\n')
self_link = f" [🔗](#{method_anchor})"
@@ -1387,7 +1401,7 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
f.write(f"{ret_type} {signature}{self_link}\n\n")
# Add method description, or a call to action if it's missing.
-
+ m.description = escape_tags(m.description)
f.write(make_deprecated_experimental(m, state))
if m.description is not None and m.description.strip() != "":
@@ -1419,7 +1433,7 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
# Create operator signature and anchor point.
operator_anchor = (
- f"class_{sanitize_class_name(class_name)}_operator_{sanitize_operator_name(m.name, state)}"
+ f"{sanitize_class_name(class_name)}_operator_{sanitize_operator_name(m.name, state)}"
)
for parameter in m.parameters:
operator_anchor += f"_{parameter.type_name.type_name}"
@@ -1429,10 +1443,11 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
f.write("\n\n")
ret_type, signature = make_method_signature(class_def, m, "", state)
+ signature = escape_tags(signature)
f.write(f"{ret_type} {signature} {self_link}\n\n")
# Add operator description, or a call to action if it's missing.
-
+ m.description = escape_tags(m.description)
f.write(make_deprecated_experimental(m, state))
if m.description is not None and m.description.strip() != "":
@@ -1464,7 +1479,7 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
# Create theme property signature and anchor point.
theme_item_anchor = (
- f"class_{sanitize_class_name(class_name)}_theme_{theme_item_def.data_name}_{theme_item_def.name}"
+ f"{sanitize_class_name(class_name)}_theme_{theme_item_def.data_name}_{theme_item_def.name}"
)
f.write(f'\n\n')
self_link = f"[🔗](#{theme_item_anchor})"
@@ -1498,6 +1513,14 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
f.write(make_footer())
+def escape_tags(input: str) -> str:
+ return input.replace("<", "<").replace(">", ">")
+
+
+def escape_braces(input: str) -> str:
+ return input.replace("{", "\\{").replace("}", "\\}")
+
+
def make_type(klass: str, state: State) -> str:
if klass.find("*") != -1: # Pointer, ignore
return f"`{klass}`"
@@ -1505,19 +1528,19 @@ def make_type(klass: str, state: State) -> str:
def resolve_type(link_type: str) -> str:
if link_type in state.classes:
filename = sanitize_class_name(link_type, True)
- return f"[{link_type}](class_{filename}.md)"
+ return f"[{link_type}]({filename}.md)"
else:
print_error(f'{state.current_class}.xml: Unresolved type "{link_type}".', state)
return f"`{link_type}`"
if klass.endswith("[]"): # Typed array, strip [] to link to contained type.
- return f"[Array](class_array.md)\\[{resolve_type(klass[: -len('[]')])}\\]"
+ return f"[Array](Array.md)\\[{resolve_type(klass[: -len('[]')])}\\]"
if klass.startswith("Dictionary["): # Typed dictionary, split elements to link contained types.
parts = klass[len("Dictionary[") : -len("]")].partition(", ")
key = parts[0]
value = parts[2]
- return f"[Dictionary](class_dictionary.md)\\[{resolve_type(key)}, {resolve_type(value)}\\]"
+ return f"[Dictionary](Dictionary.md)\\[{resolve_type(key)}, {resolve_type(value)}\\]"
return resolve_type(klass)
@@ -1543,9 +1566,9 @@ def make_enum(t: str, is_bitfield: bool, state: State) -> str:
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**\\[[{e}](class_{filename}.md#{anchor})\\]"
+ return f"**BitField**\\[[{e}]({filename}.md#{anchor})\\]"
else:
- return f"[{e}](class_{filename}.md#{anchor})"
+ return f"[{e}]({filename}.md#{anchor})"
print_error(f'{state.current_class}.xml: Unresolved enum "{t}".', state)
@@ -1568,7 +1591,9 @@ def make_method_signature(
if isinstance(definition, MethodDef) and ref_type != "":
if ref_type == "operator":
op_name = definition.name.replace("<", "\\<") # So operator "<" gets correctly displayed.
- anchor = f"class_{sanitize_class_name(class_def.name)}_{ref_type}_{sanitize_operator_name(definition.name, state)}"
+ anchor = (
+ f"{sanitize_class_name(class_def.name)}_{ref_type}_{sanitize_operator_name(definition.name, state)}"
+ )
for parameter in definition.parameters:
anchor += f"_{parameter.type_name.type_name}"
out += f"[{op_name}](#{anchor})"
@@ -1576,10 +1601,10 @@ def make_method_signature(
ref_type_qualifier = ""
if definition.name.startswith("_"):
ref_type_qualifier = "private_"
- anchor = f"class_{sanitize_class_name(class_def.name)}_{ref_type_qualifier}{ref_type}_{definition.name}"
+ anchor = f"{sanitize_class_name(class_def.name)}_{ref_type_qualifier}{ref_type}_{definition.name}"
out += f"[{definition.name}](#{anchor})"
else:
- anchor = f"class_{sanitize_class_name(class_def.name)}_{ref_type}_{definition.name}"
+ anchor = f"{sanitize_class_name(class_def.name)}_{ref_type}_{definition.name}"
out += f"[{definition.name}](#{anchor})"
else:
out += f"**{definition.name}**"
@@ -1618,7 +1643,7 @@ def make_method_signature(
for qualifier in qualifiers.split():
badge = qualifier_map.get(qualifier, f'{qualifier}')
out += f" {badge}"
-
+ out = escape_braces(out)
return ret_type, out
@@ -1703,7 +1728,7 @@ def make_separator(section_level: bool = False) -> str:
if section_level:
separator_class = "section"
- return f'
\n\n'
+ return f'
\n\n'
def make_link(url: str, title: str) -> str:
@@ -1759,7 +1784,7 @@ def make_rst_index(grouped_classes: Dict[str, List[str]], dry_run: bool, output_
if group_name in CLASS_GROUPS_BASE:
base_class = CLASS_GROUPS_BASE[group_name]
- f.write(f"- [{base_class}](class_{sanitize_class_name(base_class, True)}.md)\n")
+ f.write(f"- [{base_class}]({sanitize_class_name(base_class, True)}.md)\n")
for class_name in grouped_classes[group_name]:
if group_name in CLASS_GROUPS_BASE and sanitize_class_name(
@@ -1767,7 +1792,7 @@ def make_rst_index(grouped_classes: Dict[str, List[str]], dry_run: bool, output_
) == sanitize_class_name(class_name, True):
continue
- f.write(f"- [{class_name}](class_{sanitize_class_name(class_name, True)}.md)\n")
+ f.write(f"- [{class_name}]({sanitize_class_name(class_name, True)}.md)\n")
f.write("\n")
@@ -1905,7 +1930,7 @@ def format_text_block(
if tag_state.closing and tag_state.name == inside_code_tag:
if is_in_tagset(tag_state.name, RESERVED_CODEBLOCK_TAGS):
- tag_text = "\n```"
+ tag_text = "\n```\n"
tag_depth -= 1
inside_code = False
ignore_code_warnings = False
@@ -2226,8 +2251,8 @@ def format_text_block(
if tag_state.name == "method":
repl_text = f"{repl_text}()"
target_file = sanitize_class_name(target_class_name, True)
- anchor = f"class_{sanitize_class_name(target_class_name)}{ref_type}_{target_name}"
- tag_text = f"[{repl_text}](class_{target_file}.md#{anchor})"
+ anchor = f"{sanitize_class_name(target_class_name)}{ref_type}_{target_name}"
+ tag_text = f"[{repl_text}]({target_file}.md#{anchor})"
escape_pre = True
escape_post = True
@@ -2378,24 +2403,8 @@ def format_text_block(
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
+ if not inside_code:
+ post_text = escape_md(post_text, next_brac_pos)
text = pre_text + tag_text + post_text
pos = len(pre_text) + len(tag_text)
@@ -2467,38 +2476,60 @@ def format_context_name(context: Union[DefinitionBase, None]) -> str:
return context_name
+# Regex to match HTML tags, comments, and bracketed URLs.
+HTML_TAG_RE = re.compile(
+ r"(<(?:[a-zA-Z0-9_/]+(?:\s+[a-zA-Z0-9_/-]+(?:=\"[^\"]*\"|='[^']*'|[^>]*))?|!--.*?--|https?://.*?)>)"
+)
+
+
def escape_md(text: str, until_pos: int = -1) -> str:
# Escape \ character, otherwise it ends up as an escape character in Markdown
pos = 0
+ actual_until_pos = until_pos if until_pos != -1 else len(text)
+
while True:
- pos = text.find("\\", pos, until_pos)
+ pos = text.find("\\", pos, actual_until_pos)
if pos == -1:
break
text = f"{text[:pos]}\\\\{text[pos + 1 :]}"
pos += 2
+ actual_until_pos += 1
# Escape * character to avoid interpreting it as emphasis
pos = 0
while True:
- pos = text.find("*", pos, until_pos)
+ pos = text.find("*", pos, actual_until_pos)
if pos == -1:
break
text = f"{text[:pos]}\\*{text[pos + 1 :]}"
pos += 2
+ actual_until_pos += 1
# Escape _ character at the end of a word to avoid interpreting it as emphasis
pos = 0
while True:
- pos = text.find("_", pos, until_pos)
+ pos = text.find("_", pos, actual_until_pos)
if pos == -1:
break
- if not text[pos + 1].isalnum(): # don't escape within a snake_case word
+ if pos + 1 < len(text) and not text[pos + 1].isalnum(): # don't escape within a snake_case word
text = f"{text[:pos]}\\_{text[pos + 1 :]}"
pos += 2
+ actual_until_pos += 1
else:
pos += 1
- return text
+ # Escape < and > characters with < and > when not being used in tags or comments.
+ parts = HTML_TAG_RE.split(text[:actual_until_pos])
+ for i in range(len(parts)):
+ if i % 2 == 0: # Not a tag
+ parts[i] = parts[i].replace("<", "<").replace(">", ">")
+ else:
+ # It's a tag, but let's double check it actually looks like one.
+ # re.split with capturing group will put the whole match in parts[i]
+ if not parts[i].startswith("<") or not parts[i].endswith(">"):
+ parts[i] = parts[i].replace("<", "<").replace(">", ">")
+
+ return "".join(parts) + text[actual_until_pos:]
def format_table(f: TextIO, data: List[Tuple[Optional[str], ...]], remove_empty_columns: bool = False) -> None:
@@ -2537,12 +2568,22 @@ def format_table(f: TextIO, data: List[Tuple[Optional[str], ...]], remove_empty_
def sanitize_class_name(dirty_name: str, is_file_name=False) -> str:
if is_file_name:
- return dirty_name.lower().replace('"', "").replace("/", "--")
+ return dirty_name.replace('"', "").replace("/", "--")
else:
return dirty_name.replace('"', "").replace("/", "_").replace(".", "_")
def sanitize_operator_name(dirty_name: str, state: State) -> str:
+ """
+ Sanitizes an operator name by replacing specific patterns or values with their
+ corresponding standard names. The function attempts to map various operators
+ to a clear, pre-defined representation. If an unsupported operator is found,
+ an error message is logged.
+
+ :param dirty_name: The operator name as a string, which may require sanitization.
+ :param state: The state object, used for handling error logging purposes.
+ :return: A sanitized operator name as a string.
+ """
clear_name = dirty_name.replace("operator ", "")
if clear_name == "!=":
From 2b68ecbaf68d76d386b746a86cc7f69d359d0cda Mon Sep 17 00:00:00 2001
From: Arctis-Fireblight <6182060+Arctis-Fireblight@users.noreply.github.com>
Date: Wed, 18 Feb 2026 18:24:19 -0600
Subject: [PATCH 2/2] Improved Markdown generation in `make_md.py`:
- Updated anchor creation for consistency.
- Added `_category_.json` generation for better doc indexing.
- Introduced escaping functionality for special characters in descriptions and MD files.
- Enhanced sanitization logic for class/file/operator names.
- Streamlined Markdown reference links and tag escaping logic.
---
.pre-commit-config.yaml | 5 ++---
doc/tools/make_md.py | 39 ++++++++++++++++++++++++++-------------
2 files changed, 28 insertions(+), 16 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index a3cf6bd2c5a..0811255cec2 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,13 +1,12 @@
default_language_version:
python: python3
-# "doc/tools/make_md.py" is excluded because it fails the mypy check, and attempts to make the linter happy break the code. It needs to be the way it is.
+
exclude: |
(?x)^(
.*thirdparty/.*|
.*-(dll|dylib|so)_wrap\.[ch]|
platform/android/java/editor/src/main/java/com/android/.*|
- platform/android/java/lib/src/com/google/.*|
- doc/tools/make_md.py
+ platform/android/java/lib/src/com/google/.*
)$
repos:
diff --git a/doc/tools/make_md.py b/doc/tools/make_md.py
index 39d03bd07f0..aa45051f360 100755
--- a/doc/tools/make_md.py
+++ b/doc/tools/make_md.py
@@ -1033,7 +1033,7 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
default = property_def.default_value
if default is not None and property_def.overrides:
override_file = sanitize_class_name(property_def.overrides, True)
- ref = f"[{property_def.overrides}.{property_def.name}]({override_file}.md#class_{sanitize_class_name(property_def.overrides)}_property_{property_def.name})"
+ ref = f"[{property_def.overrides}.{property_def.name}]({override_file}.md#{sanitize_class_name(property_def.overrides)}_property_{property_def.name})"
# Not using translate() for now as it breaks table formatting.
ml.append((type_rst, property_def.name, f"{default} (overrides {ref})"))
else:
@@ -1242,7 +1242,7 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
f.write(f"{signature}{self_link}\n\n")
# Add annotation description, or a call to action if it's missing.
- m.description = escape_tags(m.description)
+ m.description = escape_tags(m.description) if m.description is not None else None
if m.description is not None and m.description.strip() != "":
f.write(f"{format_text_block(m.description.strip(), m, state)}\n\n")
else:
@@ -1304,7 +1304,7 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
f.write("\n")
# Add property description, or a call to action if it's missing.
- property_def.text = escape_tags(property_def.text)
+ property_def.text = escape_tags(property_def.text) if property_def.text is not None else None
f.write(make_deprecated_experimental(property_def, state))
if property_def.text is not None and property_def.text.strip() != "":
@@ -1353,7 +1353,7 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
f.write(f"{ret_type} {signature}{self_link}\n\n")
# Add constructor description, or a call to action if it's missing.
- m.description = escape_tags(m.description)
+ m.description = escape_tags(m.description) if m.description is not None else None
f.write(make_deprecated_experimental(m, state))
if m.description is not None and m.description.strip() != "":
@@ -1401,7 +1401,7 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
f.write(f"{ret_type} {signature}{self_link}\n\n")
# Add method description, or a call to action if it's missing.
- m.description = escape_tags(m.description)
+ m.description = escape_tags(m.description) if m.description is not None else None
f.write(make_deprecated_experimental(m, state))
if m.description is not None and m.description.strip() != "":
@@ -1443,11 +1443,11 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
f.write("\n\n")
ret_type, signature = make_method_signature(class_def, m, "", state)
- signature = escape_tags(signature)
+ signature = escape_tags(signature) if signature is not None else None
f.write(f"{ret_type} {signature} {self_link}\n\n")
# Add operator description, or a call to action if it's missing.
- m.description = escape_tags(m.description)
+ m.description = escape_tags(m.description) if m.description is not None else None
f.write(make_deprecated_experimental(m, state))
if m.description is not None and m.description.strip() != "":
@@ -1513,11 +1513,25 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
f.write(make_footer())
-def escape_tags(input: str) -> str:
- return input.replace("<", "<").replace(">", ">")
+def escape_tags(input: Optional[str]) -> str:
+ """
+ Aggressively escape < and > to < and > for MDX/Docusaurus compatibility.
+ MDX parsers are stricter than vanilla Markdown and require escaping even within
+ code blocks to prevent build failures and runtime crashes.
+ """
+ if input is None:
+ return ""
+ return input.replace("<", "<").replace(">", ">")
-def escape_braces(input: str) -> str:
+def escape_braces(input: Optional[str]) -> str:
+ """
+ Escape { and } to \{ and \} for MDX/Docusaurus compatibility.
+ MDX parsers are stricter than vanilla Markdown and require escaping even within
+ code blocks to prevent build failures and runtime crashes.
+ """
+ if input is None:
+ return ""
return input.replace("{", "\\{").replace("}", "\\}")
@@ -1628,7 +1642,7 @@ def make_method_signature(
out += "\\ ..."
out += "\\ )"
-
+ out = escape_braces(out)
if qualifiers is not None:
# Qualifiers as badges/spans for Markdown
qualifier_map = {
@@ -1643,7 +1657,6 @@ def make_method_signature(
for qualifier in qualifiers.split():
badge = qualifier_map.get(qualifier, f'{qualifier}')
out += f" {badge}"
- out = escape_braces(out)
return ret_type, out
@@ -1728,7 +1741,7 @@ def make_separator(section_level: bool = False) -> str:
if section_level:
separator_class = "section"
- return f'
\n\n'
+ return f'
\n\n'
def make_link(url: str, title: str) -> str: