Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
godotengine
GitHub Repository: godotengine/godot
Path: blob/master/doc/tools/make_rst.py
21518 views
1
#!/usr/bin/env python3
2
3
# This script makes RST files from the XML class reference for use with the online docs.
4
from __future__ import annotations
5
6
import argparse
7
import os
8
import re
9
import sys
10
import xml.etree.ElementTree as ET
11
from collections import OrderedDict
12
from typing import Any, TextIO
13
14
sys.path.insert(0, root_directory := os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../"))
15
16
import version
17
from misc.utility.color import Ansi, force_stderr_color, force_stdout_color
18
19
# $DOCS_URL/path/to/page.html(#fragment-tag)
20
GODOT_DOCS_PATTERN = re.compile(r"^\$DOCS_URL/(.*)\.html(#.*)?$")
21
22
# Based on reStructuredText inline markup recognition rules
23
# https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#inline-markup-recognition-rules
24
MARKUP_ALLOWED_PRECEDENT = " -:/'\"<([{"
25
MARKUP_ALLOWED_SUBSEQUENT = " -.,:;!?\\/'\")]}>"
26
27
# Used to translate section headings and other hardcoded strings when required with
28
# the --lang argument. The BASE_STRINGS list should be synced with what we actually
29
# write in this script (check `translate()` uses), and also hardcoded in
30
# `scripts/extract_classes.py` (godotengine/godot-editor-l10n repo) to include them in the source POT file.
31
BASE_STRINGS = [
32
"All classes",
33
"Globals",
34
"Nodes",
35
"Resources",
36
"Editor-only",
37
"Other objects",
38
"Variant types",
39
"Description",
40
"Tutorials",
41
"Properties",
42
"Constructors",
43
"Methods",
44
"Operators",
45
"Theme Properties",
46
"Signals",
47
"Enumerations",
48
"Constants",
49
"Annotations",
50
"Property Descriptions",
51
"Constructor Descriptions",
52
"Method Descriptions",
53
"Operator Descriptions",
54
"Theme Property Descriptions",
55
"Inherits:",
56
"Inherited By:",
57
"(overrides %s)",
58
"Default",
59
"Setter",
60
"value",
61
"Getter",
62
"This method should typically be overridden by the user to have any effect.",
63
"This method is required to be overridden when extending its base class.",
64
"This method has no side effects. It doesn't modify any of the instance's member variables.",
65
"This method accepts any number of arguments after the ones described here.",
66
"This method is used to construct a type.",
67
"This method doesn't need an instance to be called, so it can be called directly using the class name.",
68
"This method describes a valid operator to use with this type as left-hand operand.",
69
"This value is an integer composed as a bitmask of the following flags.",
70
"No return value.",
71
"There is currently no description for this class. Please help us by `contributing one <https://contributing.godotengine.org/en/latest/documentation/class_reference.html>`__!",
72
"There is currently no description for this signal. Please help us by `contributing one <https://contributing.godotengine.org/en/latest/documentation/class_reference.html>`__!",
73
"There is currently no description for this enum. Please help us by `contributing one <https://contributing.godotengine.org/en/latest/documentation/class_reference.html>`__!",
74
"There is currently no description for this constant. Please help us by `contributing one <https://contributing.godotengine.org/en/latest/documentation/class_reference.html>`__!",
75
"There is currently no description for this annotation. Please help us by `contributing one <https://contributing.godotengine.org/en/latest/documentation/class_reference.html>`__!",
76
"There is currently no description for this property. Please help us by `contributing one <https://contributing.godotengine.org/en/latest/documentation/class_reference.html>`__!",
77
"There is currently no description for this constructor. Please help us by `contributing one <https://contributing.godotengine.org/en/latest/documentation/class_reference.html>`__!",
78
"There is currently no description for this method. Please help us by `contributing one <https://contributing.godotengine.org/en/latest/documentation/class_reference.html>`__!",
79
"There is currently no description for this operator. Please help us by `contributing one <https://contributing.godotengine.org/en/latest/documentation/class_reference.html>`__!",
80
"There is currently no description for this theme property. Please help us by `contributing one <https://contributing.godotengine.org/en/latest/documentation/class_reference.html>`__!",
81
"There are notable differences when using this API with C#. See :ref:`doc_c_sharp_differences` for more information.",
82
"Deprecated:",
83
"Experimental:",
84
"This signal may be changed or removed in future versions.",
85
"This constant may be changed or removed in future versions.",
86
"This property may be changed or removed in future versions.",
87
"This constructor may be changed or removed in future versions.",
88
"This method may be changed or removed in future versions.",
89
"This operator may be changed or removed in future versions.",
90
"This theme property may be changed or removed in future versions.",
91
# See also `make_rst_class()` and `editor/doc/editor_help.cpp`.
92
"[b]Note:[/b] The returned array is [i]copied[/i] and any changes to it will not update the original property value. See [%s] for more details.",
93
]
94
strings_l10n: dict[str, str] = {}
95
writing_translation = False
96
97
CLASS_GROUPS: dict[str, str] = {
98
"global": "Globals",
99
"node": "Nodes",
100
"resource": "Resources",
101
"object": "Other objects",
102
"editor": "Editor-only",
103
"variant": "Variant types",
104
}
105
CLASS_GROUPS_BASE: dict[str, str] = {
106
"node": "Node",
107
"resource": "Resource",
108
"object": "Object",
109
"variant": "Variant",
110
}
111
# Sync with editor\register_editor_types.cpp
112
EDITOR_CLASSES: list[str] = [
113
"FileSystemDock",
114
"ScriptCreateDialog",
115
"ScriptEditor",
116
"ScriptEditorBase",
117
]
118
# Sync with the types mentioned in https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/c_sharp_differences.html
119
CLASSES_WITH_CSHARP_DIFFERENCES: list[str] = [
120
"@GlobalScope",
121
"String",
122
"StringName",
123
"NodePath",
124
"Signal",
125
"Callable",
126
"RID",
127
"Basis",
128
"Transform2D",
129
"Transform3D",
130
"Rect2",
131
"Rect2i",
132
"AABB",
133
"Quaternion",
134
"Projection",
135
"Color",
136
"Array",
137
"Dictionary",
138
"PackedByteArray",
139
"PackedColorArray",
140
"PackedFloat32Array",
141
"PackedFloat64Array",
142
"PackedInt32Array",
143
"PackedInt64Array",
144
"PackedStringArray",
145
"PackedVector2Array",
146
"PackedVector3Array",
147
"PackedVector4Array",
148
"Variant",
149
]
150
151
PACKED_ARRAY_TYPES: list[str] = [
152
"PackedByteArray",
153
"PackedColorArray",
154
"PackedFloat32Array",
155
"PackedFloat64Array",
156
"PackedInt32Array",
157
"PackedInt64Array",
158
"PackedStringArray",
159
"PackedVector2Array",
160
"PackedVector3Array",
161
"PackedVector4Array",
162
]
163
164
165
class State:
166
def __init__(self) -> None:
167
self.num_errors = 0
168
self.num_warnings = 0
169
self.classes: OrderedDict[str, ClassDef] = OrderedDict()
170
self.current_class: str = ""
171
172
# Additional content and structure checks and validators.
173
self.script_language_parity_check: ScriptLanguageParityCheck = ScriptLanguageParityCheck()
174
175
def parse_class(self, class_root: ET.Element, filepath: str) -> None:
176
class_name = class_root.attrib["name"]
177
self.current_class = class_name
178
179
class_def = ClassDef(class_name)
180
self.classes[class_name] = class_def
181
class_def.filepath = filepath
182
183
inherits = class_root.get("inherits")
184
if inherits is not None:
185
class_def.inherits = inherits
186
187
class_def.deprecated = class_root.get("deprecated")
188
class_def.experimental = class_root.get("experimental")
189
190
brief_desc = class_root.find("brief_description")
191
if brief_desc is not None and brief_desc.text:
192
class_def.brief_description = brief_desc.text
193
194
desc = class_root.find("description")
195
if desc is not None and desc.text:
196
class_def.description = desc.text
197
198
keywords = class_root.get("keywords")
199
if keywords is not None:
200
class_def.keywords = keywords
201
202
properties = class_root.find("members")
203
if properties is not None:
204
for property in properties:
205
assert property.tag == "member"
206
207
property_name = property.attrib["name"]
208
if property_name in class_def.properties:
209
print_error(f'{class_name}.xml: Duplicate property "{property_name}".', self)
210
continue
211
212
type_name = TypeName.from_element(property)
213
setter = property.get("setter") or None # Use or None so '' gets turned into None.
214
getter = property.get("getter") or None
215
default_value = property.get("default") or None
216
if default_value is not None:
217
default_value = f"``{default_value}``"
218
overrides = property.get("overrides") or None
219
220
property_def = PropertyDef(
221
property_name, type_name, setter, getter, property.text, default_value, overrides
222
)
223
property_def.deprecated = property.get("deprecated")
224
property_def.experimental = property.get("experimental")
225
class_def.properties[property_name] = property_def
226
227
constructors = class_root.find("constructors")
228
if constructors is not None:
229
for constructor in constructors:
230
assert constructor.tag == "constructor"
231
232
method_name = constructor.attrib["name"]
233
qualifiers = constructor.get("qualifiers")
234
235
return_element = constructor.find("return")
236
if return_element is not None:
237
return_type = TypeName.from_element(return_element)
238
else:
239
return_type = TypeName("void")
240
241
params = self.parse_params(constructor, "constructor")
242
243
desc_element = constructor.find("description")
244
method_desc = None
245
if desc_element is not None:
246
method_desc = desc_element.text
247
248
method_def = MethodDef(method_name, return_type, params, method_desc, qualifiers)
249
method_def.definition_name = "constructor"
250
method_def.deprecated = constructor.get("deprecated")
251
method_def.experimental = constructor.get("experimental")
252
if method_name not in class_def.constructors:
253
class_def.constructors[method_name] = []
254
255
class_def.constructors[method_name].append(method_def)
256
257
methods = class_root.find("methods")
258
if methods is not None:
259
for method in methods:
260
assert method.tag == "method"
261
262
method_name = method.attrib["name"]
263
qualifiers = method.get("qualifiers")
264
265
return_element = method.find("return")
266
if return_element is not None:
267
return_type = TypeName.from_element(return_element)
268
269
else:
270
return_type = TypeName("void")
271
272
params = self.parse_params(method, "method")
273
274
desc_element = method.find("description")
275
method_desc = None
276
if desc_element is not None:
277
method_desc = desc_element.text
278
279
method_def = MethodDef(method_name, return_type, params, method_desc, qualifiers)
280
method_def.deprecated = method.get("deprecated")
281
method_def.experimental = method.get("experimental")
282
if method_name not in class_def.methods:
283
class_def.methods[method_name] = []
284
285
class_def.methods[method_name].append(method_def)
286
287
operators = class_root.find("operators")
288
if operators is not None:
289
for operator in operators:
290
assert operator.tag == "operator"
291
292
method_name = operator.attrib["name"]
293
qualifiers = operator.get("qualifiers")
294
295
return_element = operator.find("return")
296
if return_element is not None:
297
return_type = TypeName.from_element(return_element)
298
299
else:
300
return_type = TypeName("void")
301
302
params = self.parse_params(operator, "operator")
303
304
desc_element = operator.find("description")
305
method_desc = None
306
if desc_element is not None:
307
method_desc = desc_element.text
308
309
method_def = MethodDef(method_name, return_type, params, method_desc, qualifiers)
310
method_def.definition_name = "operator"
311
method_def.deprecated = operator.get("deprecated")
312
method_def.experimental = operator.get("experimental")
313
if method_name not in class_def.operators:
314
class_def.operators[method_name] = []
315
316
class_def.operators[method_name].append(method_def)
317
318
constants = class_root.find("constants")
319
if constants is not None:
320
for constant in constants:
321
assert constant.tag == "constant"
322
323
constant_name = constant.attrib["name"]
324
value = constant.attrib["value"]
325
enum = constant.get("enum")
326
is_bitfield = constant.get("is_bitfield") == "true"
327
constant_def = ConstantDef(constant_name, value, constant.text, is_bitfield)
328
constant_def.deprecated = constant.get("deprecated")
329
constant_def.experimental = constant.get("experimental")
330
if enum is None:
331
if constant_name in class_def.constants:
332
print_error(f'{class_name}.xml: Duplicate constant "{constant_name}".', self)
333
continue
334
335
class_def.constants[constant_name] = constant_def
336
337
else:
338
if enum in class_def.enums:
339
enum_def = class_def.enums[enum]
340
341
else:
342
enum_def = EnumDef(enum, TypeName("int", enum), is_bitfield)
343
class_def.enums[enum] = enum_def
344
345
enum_def.values[constant_name] = constant_def
346
347
annotations = class_root.find("annotations")
348
if annotations is not None:
349
for annotation in annotations:
350
assert annotation.tag == "annotation"
351
352
annotation_name = annotation.attrib["name"]
353
qualifiers = annotation.get("qualifiers")
354
355
params = self.parse_params(annotation, "annotation")
356
357
desc_element = annotation.find("description")
358
annotation_desc = None
359
if desc_element is not None:
360
annotation_desc = desc_element.text
361
362
annotation_def = AnnotationDef(annotation_name, params, annotation_desc, qualifiers)
363
if annotation_name not in class_def.annotations:
364
class_def.annotations[annotation_name] = []
365
366
class_def.annotations[annotation_name].append(annotation_def)
367
368
signals = class_root.find("signals")
369
if signals is not None:
370
for signal in signals:
371
assert signal.tag == "signal"
372
373
signal_name = signal.attrib["name"]
374
375
if signal_name in class_def.signals:
376
print_error(f'{class_name}.xml: Duplicate signal "{signal_name}".', self)
377
continue
378
379
params = self.parse_params(signal, "signal")
380
381
desc_element = signal.find("description")
382
signal_desc = None
383
if desc_element is not None:
384
signal_desc = desc_element.text
385
386
signal_def = SignalDef(signal_name, params, signal_desc)
387
signal_def.deprecated = signal.get("deprecated")
388
signal_def.experimental = signal.get("experimental")
389
class_def.signals[signal_name] = signal_def
390
391
theme_items = class_root.find("theme_items")
392
if theme_items is not None:
393
for theme_item in theme_items:
394
assert theme_item.tag == "theme_item"
395
396
theme_item_name = theme_item.attrib["name"]
397
theme_item_data_name = theme_item.attrib["data_type"]
398
theme_item_id = "{}_{}".format(theme_item_data_name, theme_item_name)
399
if theme_item_id in class_def.theme_items:
400
print_error(
401
f'{class_name}.xml: Duplicate theme property "{theme_item_name}" of type "{theme_item_data_name}".',
402
self,
403
)
404
continue
405
406
default_value = theme_item.get("default") or None
407
if default_value is not None:
408
default_value = f"``{default_value}``"
409
410
theme_item_def = ThemeItemDef(
411
theme_item_name,
412
TypeName.from_element(theme_item),
413
theme_item_data_name,
414
theme_item.text,
415
default_value,
416
)
417
class_def.theme_items[theme_item_name] = theme_item_def
418
419
tutorials = class_root.find("tutorials")
420
if tutorials is not None:
421
for link in tutorials:
422
assert link.tag == "link"
423
424
if link.text is not None:
425
class_def.tutorials.append((link.text.strip(), link.get("title", "")))
426
427
self.current_class = ""
428
429
def parse_params(self, root: ET.Element, context: str) -> list[ParameterDef]:
430
param_elements = root.findall("param")
431
params: Any = [None] * len(param_elements)
432
433
for param_index, param_element in enumerate(param_elements):
434
param_name = param_element.attrib["name"]
435
index = int(param_element.attrib["index"])
436
type_name = TypeName.from_element(param_element)
437
default = param_element.get("default")
438
439
if param_name.strip() == "" or param_name.startswith("_unnamed_arg"):
440
print_error(
441
f'{self.current_class}.xml: Empty argument name in {context} "{root.attrib["name"]}" at position {param_index}.',
442
self,
443
)
444
445
params[index] = ParameterDef(param_name, type_name, default)
446
447
cast: list[ParameterDef] = params
448
449
return cast
450
451
def sort_classes(self) -> None:
452
self.classes = OrderedDict(sorted(self.classes.items(), key=lambda t: t[0].lower()))
453
454
455
class TagState:
456
def __init__(self, raw: str, name: str, arguments: str, closing: bool) -> None:
457
self.raw = raw
458
459
self.name = name
460
self.arguments = arguments
461
self.closing = closing
462
463
464
class TypeName:
465
def __init__(self, type_name: str, enum: str | None = None, is_bitfield: bool = False) -> None:
466
self.type_name = type_name
467
self.enum = enum
468
self.is_bitfield = is_bitfield
469
470
def to_rst(self, state: State) -> str:
471
if self.enum is not None:
472
return make_enum(self.enum, self.is_bitfield, state)
473
elif self.type_name == "void":
474
return "|void|"
475
else:
476
return make_type(self.type_name, state)
477
478
@classmethod
479
def from_element(cls, element: ET.Element) -> TypeName:
480
return cls(element.attrib["type"], element.get("enum"), element.get("is_bitfield") == "true")
481
482
483
class DefinitionBase:
484
def __init__(
485
self,
486
definition_name: str,
487
name: str,
488
) -> None:
489
self.definition_name = definition_name
490
self.name = name
491
self.deprecated: str | None = None
492
self.experimental: str | None = None
493
494
495
class PropertyDef(DefinitionBase):
496
def __init__(
497
self,
498
name: str,
499
type_name: TypeName,
500
setter: str | None,
501
getter: str | None,
502
text: str | None,
503
default_value: str | None,
504
overrides: str | None,
505
) -> None:
506
super().__init__("property", name)
507
508
self.type_name = type_name
509
self.setter = setter
510
self.getter = getter
511
self.text = text
512
self.default_value = default_value
513
self.overrides = overrides
514
515
516
class ParameterDef(DefinitionBase):
517
def __init__(self, name: str, type_name: TypeName, default_value: str | None) -> None:
518
super().__init__("parameter", name)
519
520
self.type_name = type_name
521
self.default_value = default_value
522
523
524
class SignalDef(DefinitionBase):
525
def __init__(self, name: str, parameters: list[ParameterDef], description: str | None) -> None:
526
super().__init__("signal", name)
527
528
self.parameters = parameters
529
self.description = description
530
531
532
class AnnotationDef(DefinitionBase):
533
def __init__(
534
self,
535
name: str,
536
parameters: list[ParameterDef],
537
description: str | None,
538
qualifiers: str | None,
539
) -> None:
540
super().__init__("annotation", name)
541
542
self.parameters = parameters
543
self.description = description
544
self.qualifiers = qualifiers
545
546
547
class MethodDef(DefinitionBase):
548
def __init__(
549
self,
550
name: str,
551
return_type: TypeName,
552
parameters: list[ParameterDef],
553
description: str | None,
554
qualifiers: str | None,
555
) -> None:
556
super().__init__("method", name)
557
558
self.return_type = return_type
559
self.parameters = parameters
560
self.description = description
561
self.qualifiers = qualifiers
562
563
564
class ConstantDef(DefinitionBase):
565
def __init__(self, name: str, value: str, text: str | None, bitfield: bool) -> None:
566
super().__init__("constant", name)
567
568
self.value = value
569
self.text = text
570
self.is_bitfield = bitfield
571
572
573
class EnumDef(DefinitionBase):
574
def __init__(self, name: str, type_name: TypeName, bitfield: bool) -> None:
575
super().__init__("enum", name)
576
577
self.type_name = type_name
578
self.values: OrderedDict[str, ConstantDef] = OrderedDict()
579
self.is_bitfield = bitfield
580
581
582
class ThemeItemDef(DefinitionBase):
583
def __init__(
584
self, name: str, type_name: TypeName, data_name: str, text: str | None, default_value: str | None
585
) -> None:
586
super().__init__("theme property", name)
587
588
self.type_name = type_name
589
self.data_name = data_name
590
self.text = text
591
self.default_value = default_value
592
593
594
class ClassDef(DefinitionBase):
595
def __init__(self, name: str) -> None:
596
super().__init__("class", name)
597
598
self.class_group = "variant"
599
self.editor_class = self._is_editor_class()
600
601
self.constants: OrderedDict[str, ConstantDef] = OrderedDict()
602
self.enums: OrderedDict[str, EnumDef] = OrderedDict()
603
self.properties: OrderedDict[str, PropertyDef] = OrderedDict()
604
self.constructors: OrderedDict[str, list[MethodDef]] = OrderedDict()
605
self.methods: OrderedDict[str, list[MethodDef]] = OrderedDict()
606
self.operators: OrderedDict[str, list[MethodDef]] = OrderedDict()
607
self.signals: OrderedDict[str, SignalDef] = OrderedDict()
608
self.annotations: OrderedDict[str, list[AnnotationDef]] = OrderedDict()
609
self.theme_items: OrderedDict[str, ThemeItemDef] = OrderedDict()
610
self.inherits: str | None = None
611
self.brief_description: str | None = None
612
self.description: str | None = None
613
self.tutorials: list[tuple[str, str]] = []
614
self.keywords: str | None = None
615
616
# Used to match the class with XML source for output filtering purposes.
617
self.filepath: str = ""
618
619
def _is_editor_class(self) -> bool:
620
if self.name.startswith("Editor"):
621
return True
622
if self.name in EDITOR_CLASSES:
623
return True
624
625
return False
626
627
def update_class_group(self, state: State) -> None:
628
group_name = "variant"
629
630
if self.name.startswith("@"):
631
group_name = "global"
632
elif self.inherits:
633
inherits = self.inherits.strip()
634
635
while inherits in state.classes:
636
if inherits == "Node":
637
group_name = "node"
638
break
639
if inherits == "Resource":
640
group_name = "resource"
641
break
642
if inherits == "Object":
643
group_name = "object"
644
break
645
646
inode = state.classes[inherits].inherits
647
if inode:
648
inherits = inode.strip()
649
else:
650
break
651
652
self.class_group = group_name
653
654
655
# Checks if code samples have both GDScript and C# variations.
656
# For simplicity we assume that a GDScript example is always present, and ignore contexts
657
# which don't necessarily need C# examples.
658
class ScriptLanguageParityCheck:
659
def __init__(self) -> None:
660
self.hit_map: OrderedDict[str, list[tuple[DefinitionBase, str]]] = OrderedDict()
661
self.hit_count = 0
662
663
def add_hit(self, class_name: str, context: DefinitionBase, error: str, state: State) -> None:
664
if class_name in ["@GDScript", "@GlobalScope"]:
665
return # We don't expect these contexts to have parity.
666
667
class_def = state.classes[class_name]
668
if class_def.class_group == "variant" and class_def.name != "Object":
669
return # Variant types are replaced with native types in C#, we don't expect parity.
670
671
self.hit_count += 1
672
673
if class_name not in self.hit_map:
674
self.hit_map[class_name] = []
675
676
self.hit_map[class_name].append((context, error))
677
678
679
# Entry point for the RST generator.
680
def main() -> None:
681
parser = argparse.ArgumentParser()
682
parser.add_argument("path", nargs="+", help="A path to an XML file or a directory containing XML files to parse.")
683
parser.add_argument("--filter", default="", help="The filepath pattern for XML files to filter.")
684
parser.add_argument("--lang", "-l", default="en", help="Language to use for section headings.")
685
parser.add_argument(
686
"--color",
687
action="store_true",
688
help="If passed, force colored output even if stdout is not a TTY (useful for continuous integration).",
689
)
690
group = parser.add_mutually_exclusive_group()
691
group.add_argument("--output", "-o", default=".", help="The directory to save output .rst files in.")
692
group.add_argument(
693
"--dry-run",
694
action="store_true",
695
help="If passed, no output will be generated and XML files are only checked for errors.",
696
)
697
parser.add_argument(
698
"--verbose",
699
action="store_true",
700
help="If passed, enables verbose printing.",
701
)
702
args = parser.parse_args()
703
704
if args.color:
705
force_stdout_color(True)
706
force_stderr_color(True)
707
708
# Retrieve heading translations for the given language.
709
if not args.dry_run and args.lang != "en":
710
global writing_translation
711
writing_translation = True
712
lang_file = os.path.join(
713
os.path.dirname(os.path.realpath(__file__)), "..", "translations", "{}.po".format(args.lang)
714
)
715
if os.path.exists(lang_file):
716
try:
717
import polib # type: ignore
718
except ImportError:
719
print("Base template strings localization requires `polib`.")
720
exit(1)
721
722
pofile = polib.pofile(lang_file)
723
for entry in pofile.translated_entries():
724
if entry.msgid in BASE_STRINGS:
725
strings_l10n[entry.msgid] = entry.msgstr
726
else:
727
print(f'No PO file at "{lang_file}" for language "{args.lang}".')
728
729
print("Checking for errors in the XML class reference...")
730
731
file_list: list[str] = []
732
733
for path in args.path:
734
# Cut off trailing slashes so os.path.basename doesn't choke.
735
if path.endswith("/") or path.endswith("\\"):
736
path = path[:-1]
737
738
if os.path.basename(path) in ["modules", "platform"]:
739
for subdir, dirs, _ in os.walk(path):
740
if "doc_classes" in dirs:
741
doc_dir = os.path.join(subdir, "doc_classes")
742
class_file_names = (f for f in os.listdir(doc_dir) if f.endswith(".xml"))
743
file_list += (os.path.join(doc_dir, f) for f in class_file_names)
744
745
elif os.path.isdir(path):
746
file_list += (os.path.join(path, f) for f in os.listdir(path) if f.endswith(".xml"))
747
748
elif os.path.isfile(path):
749
if not path.endswith(".xml"):
750
print(f'Got non-.xml file "{path}" in input, skipping.')
751
continue
752
753
file_list.append(path)
754
755
classes: dict[str, tuple[ET.Element, str]] = {}
756
state = State()
757
758
for cur_file in file_list:
759
try:
760
tree = ET.parse(cur_file)
761
except ET.ParseError as e:
762
print_error(f"{cur_file}: Parse error while reading the file: {e}", state)
763
continue
764
doc = tree.getroot()
765
766
name = doc.attrib["name"]
767
if name in classes:
768
print_error(f'{cur_file}: Duplicate class "{name}".', state)
769
continue
770
771
classes[name] = (doc, cur_file)
772
773
for name, data in classes.items():
774
try:
775
state.parse_class(data[0], data[1])
776
except Exception as e:
777
print_error(f"{name}.xml: Exception while parsing class: {e}", state)
778
779
state.sort_classes()
780
781
pattern = re.compile(args.filter)
782
783
# Create the output folder recursively if it doesn't already exist.
784
os.makedirs(args.output, exist_ok=True)
785
786
print("Generating the RST class reference...")
787
788
grouped_classes: dict[str, list[str]] = {}
789
790
for class_name, class_def in state.classes.items():
791
if args.filter and not pattern.search(class_def.filepath):
792
continue
793
state.current_class = class_name
794
795
class_def.update_class_group(state)
796
make_rst_class(class_def, state, args.dry_run, args.output)
797
798
if class_def.class_group not in grouped_classes:
799
grouped_classes[class_def.class_group] = []
800
grouped_classes[class_def.class_group].append(class_name)
801
802
if class_def.editor_class:
803
if "editor" not in grouped_classes:
804
grouped_classes["editor"] = []
805
grouped_classes["editor"].append(class_name)
806
807
print("")
808
print("Generating the index file...")
809
810
make_rst_index(grouped_classes, args.dry_run, args.output)
811
812
print("")
813
814
# Print out checks.
815
816
if state.script_language_parity_check.hit_count > 0:
817
if not args.verbose:
818
print(
819
f"{Ansi.YELLOW}{state.script_language_parity_check.hit_count} code samples failed parity check. Use --verbose to get more information.{Ansi.RESET}"
820
)
821
else:
822
print(
823
f"{Ansi.YELLOW}{state.script_language_parity_check.hit_count} code samples failed parity check:{Ansi.RESET}"
824
)
825
826
for class_name in state.script_language_parity_check.hit_map.keys():
827
class_hits = state.script_language_parity_check.hit_map[class_name]
828
print(f'{Ansi.YELLOW}- {len(class_hits)} hits in class "{class_name}"{Ansi.RESET}')
829
830
for context, error in class_hits:
831
print(f" - {error} in {format_context_name(context)}")
832
print("")
833
834
# Print out warnings and errors, or lack thereof, and exit with an appropriate code.
835
836
if state.num_warnings >= 2:
837
print(
838
f"{Ansi.YELLOW}{state.num_warnings} warnings were found in the class reference XML. Please check the messages above.{Ansi.RESET}"
839
)
840
elif state.num_warnings == 1:
841
print(
842
f"{Ansi.YELLOW}1 warning was found in the class reference XML. Please check the messages above.{Ansi.RESET}"
843
)
844
845
if state.num_errors >= 2:
846
print(
847
f"{Ansi.RED}{state.num_errors} errors were found in the class reference XML. Please check the messages above.{Ansi.RESET}"
848
)
849
elif state.num_errors == 1:
850
print(f"{Ansi.RED}1 error was found in the class reference XML. Please check the messages above.{Ansi.RESET}")
851
852
if state.num_warnings == 0 and state.num_errors == 0:
853
print(f"{Ansi.GREEN}No warnings or errors found in the class reference XML.{Ansi.RESET}")
854
if not args.dry_run:
855
print(f"Wrote reStructuredText files for each class to: {args.output}")
856
else:
857
exit(1)
858
859
860
# Common helpers.
861
862
863
def print_error(error: str, state: State) -> None:
864
print(f"{Ansi.RED}{Ansi.BOLD}ERROR:{Ansi.REGULAR} {error}{Ansi.RESET}")
865
state.num_errors += 1
866
867
868
def print_warning(warning: str, state: State) -> None:
869
print(f"{Ansi.YELLOW}{Ansi.BOLD}WARNING:{Ansi.REGULAR} {warning}{Ansi.RESET}")
870
state.num_warnings += 1
871
872
873
def translate(string: str) -> str:
874
"""Translate a string based on translations sourced from `doc/translations/*.po`
875
for a language if defined via the --lang command line argument.
876
Returns the original string if no translation exists.
877
"""
878
return strings_l10n.get(string, string)
879
880
881
def get_git_branch() -> str:
882
if hasattr(version, "docs") and version.docs != "latest":
883
return version.docs
884
885
return "master"
886
887
888
# Generator methods.
889
890
891
def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir: str) -> None:
892
class_name = class_def.name
893
with open(
894
os.devnull if dry_run else os.path.join(output_dir, f"class_{sanitize_class_name(class_name, True)}.rst"),
895
"w",
896
encoding="utf-8",
897
newline="\n",
898
) as f:
899
# Remove the "Edit on Github" button from the online docs page.
900
f.write(":github_url: hide\n\n")
901
902
# Add keywords metadata.
903
if class_def.keywords is not None and class_def.keywords != "":
904
f.write(f".. meta::\n\t:keywords: {class_def.keywords}\n\n")
905
906
if not writing_translation: # Skip for translations to reduce diff.
907
# Warn contributors not to edit this file directly.
908
# Also provide links to the source files for reference.
909
git_branch = get_git_branch()
910
source_xml_path = os.path.relpath(class_def.filepath, root_directory).replace("\\", "/")
911
source_github_url = f"https://github.com/godotengine/godot/tree/{git_branch}/{source_xml_path}"
912
generator_github_url = f"https://github.com/godotengine/godot/tree/{git_branch}/doc/tools/make_rst.py"
913
914
f.write(".. DO NOT EDIT THIS FILE!!!\n")
915
f.write(".. Generated automatically from Godot engine sources.\n")
916
f.write(f".. Generator: {generator_github_url}.\n")
917
f.write(f".. XML source: {source_github_url}.\n\n")
918
919
# Document reference id and header.
920
f.write(f".. _class_{sanitize_class_name(class_name)}:\n\n")
921
f.write(make_heading(class_name, "=", False))
922
923
f.write(make_deprecated_experimental(class_def, state))
924
925
### INHERITANCE TREE ###
926
927
# Ascendants
928
if class_def.inherits:
929
inherits = class_def.inherits.strip()
930
f.write(f"**{translate('Inherits:')}** ")
931
first = True
932
while inherits is not None:
933
if not first:
934
f.write(" **<** ")
935
else:
936
first = False
937
938
f.write(make_type(inherits, state))
939
940
if inherits not in state.classes:
941
break # Parent unknown.
942
943
inode = state.classes[inherits].inherits
944
if inode:
945
inherits = inode.strip()
946
else:
947
break
948
f.write("\n\n")
949
950
# Descendants
951
inherited: list[str] = []
952
for c in state.classes.values():
953
if c.inherits and c.inherits.strip() == class_name:
954
inherited.append(c.name)
955
956
if len(inherited):
957
f.write(f"**{translate('Inherited By:')}** ")
958
for i, child in enumerate(inherited):
959
if i > 0:
960
f.write(", ")
961
f.write(make_type(child, state))
962
f.write("\n\n")
963
964
### INTRODUCTION ###
965
966
has_description = False
967
968
# Brief description
969
if class_def.brief_description is not None and class_def.brief_description.strip() != "":
970
has_description = True
971
972
f.write(f"{format_text_block(class_def.brief_description.strip(), class_def, state)}\n\n")
973
974
# Class description
975
if class_def.description is not None and class_def.description.strip() != "":
976
has_description = True
977
978
f.write(".. rst-class:: classref-introduction-group\n\n")
979
f.write(make_heading("Description", "-"))
980
981
f.write(f"{format_text_block(class_def.description.strip(), class_def, state)}\n\n")
982
983
if not has_description:
984
f.write(".. container:: contribute\n\n\t")
985
f.write(
986
translate(
987
"There is currently no description for this class. Please help us by `contributing one <https://contributing.godotengine.org/en/latest/documentation/class_reference.html>`__!"
988
)
989
+ "\n\n"
990
)
991
992
if class_def.name in CLASSES_WITH_CSHARP_DIFFERENCES:
993
f.write(".. note::\n\n\t")
994
f.write(
995
translate(
996
"There are notable differences when using this API with C#. See :ref:`doc_c_sharp_differences` for more information."
997
)
998
+ "\n\n"
999
)
1000
1001
# Online tutorials
1002
if len(class_def.tutorials) > 0:
1003
f.write(".. rst-class:: classref-introduction-group\n\n")
1004
f.write(make_heading("Tutorials", "-"))
1005
1006
for url, title in class_def.tutorials:
1007
f.write(f"- {make_link(url, title)}\n\n")
1008
1009
### REFERENCE TABLES ###
1010
1011
# Reused container for reference tables.
1012
ml: list[tuple[str | None, ...]] = []
1013
1014
# Properties reference table
1015
if len(class_def.properties) > 0:
1016
f.write(".. rst-class:: classref-reftable-group\n\n")
1017
f.write(make_heading("Properties", "-"))
1018
1019
ml = []
1020
for property_def in class_def.properties.values():
1021
type_rst = property_def.type_name.to_rst(state)
1022
default = property_def.default_value
1023
if default is not None and property_def.overrides:
1024
ref = f":ref:`{property_def.overrides}<class_{sanitize_class_name(property_def.overrides)}_property_{property_def.name}>`"
1025
# Not using translate() for now as it breaks table formatting.
1026
ml.append((type_rst, property_def.name, f"{default} (overrides {ref})"))
1027
else:
1028
ref = f":ref:`{property_def.name}<class_{sanitize_class_name(class_name)}_property_{property_def.name}>`"
1029
ml.append((type_rst, ref, default))
1030
1031
format_table(f, ml, True)
1032
1033
# Constructors, Methods, Operators reference tables
1034
if len(class_def.constructors) > 0:
1035
f.write(".. rst-class:: classref-reftable-group\n\n")
1036
f.write(make_heading("Constructors", "-"))
1037
1038
ml = []
1039
for method_list in class_def.constructors.values():
1040
for m in method_list:
1041
ml.append(make_method_signature(class_def, m, "constructor", state))
1042
1043
format_table(f, ml)
1044
1045
if len(class_def.methods) > 0:
1046
f.write(".. rst-class:: classref-reftable-group\n\n")
1047
f.write(make_heading("Methods", "-"))
1048
1049
ml = []
1050
for method_list in class_def.methods.values():
1051
for m in method_list:
1052
ml.append(make_method_signature(class_def, m, "method", state))
1053
1054
format_table(f, ml)
1055
1056
if len(class_def.operators) > 0:
1057
f.write(".. rst-class:: classref-reftable-group\n\n")
1058
f.write(make_heading("Operators", "-"))
1059
1060
ml = []
1061
for method_list in class_def.operators.values():
1062
for m in method_list:
1063
ml.append(make_method_signature(class_def, m, "operator", state))
1064
1065
format_table(f, ml)
1066
1067
# Theme properties reference table
1068
if len(class_def.theme_items) > 0:
1069
f.write(".. rst-class:: classref-reftable-group\n\n")
1070
f.write(make_heading("Theme Properties", "-"))
1071
1072
ml = []
1073
for theme_item_def in class_def.theme_items.values():
1074
ref = f":ref:`{theme_item_def.name}<class_{sanitize_class_name(class_name)}_theme_{theme_item_def.data_name}_{theme_item_def.name}>`"
1075
ml.append((theme_item_def.type_name.to_rst(state), ref, theme_item_def.default_value))
1076
1077
format_table(f, ml, True)
1078
1079
### DETAILED DESCRIPTIONS ###
1080
1081
# Signal descriptions
1082
if len(class_def.signals) > 0:
1083
f.write(make_separator(True))
1084
f.write(".. rst-class:: classref-descriptions-group\n\n")
1085
f.write(make_heading("Signals", "-"))
1086
1087
index = 0
1088
1089
for signal in class_def.signals.values():
1090
if index != 0:
1091
f.write(make_separator())
1092
1093
# Create signal signature and anchor point.
1094
1095
signal_anchor = f"class_{sanitize_class_name(class_name)}_signal_{signal.name}"
1096
f.write(f".. _{signal_anchor}:\n\n")
1097
self_link = f":ref:`🔗<{signal_anchor}>`"
1098
f.write(".. rst-class:: classref-signal\n\n")
1099
1100
_, signature = make_method_signature(class_def, signal, "", state)
1101
f.write(f"{signature} {self_link}\n\n")
1102
1103
# Add signal description, or a call to action if it's missing.
1104
1105
f.write(make_deprecated_experimental(signal, state))
1106
1107
if signal.description is not None and signal.description.strip() != "":
1108
f.write(f"{format_text_block(signal.description.strip(), signal, state)}\n\n")
1109
elif signal.deprecated is None and signal.experimental is None:
1110
f.write(".. container:: contribute\n\n\t")
1111
f.write(
1112
translate(
1113
"There is currently no description for this signal. Please help us by `contributing one <https://contributing.godotengine.org/en/latest/documentation/class_reference.html>`__!"
1114
)
1115
+ "\n\n"
1116
)
1117
1118
index += 1
1119
1120
# Enumeration descriptions
1121
if len(class_def.enums) > 0:
1122
f.write(make_separator(True))
1123
f.write(".. rst-class:: classref-descriptions-group\n\n")
1124
f.write(make_heading("Enumerations", "-"))
1125
1126
index = 0
1127
1128
for e in class_def.enums.values():
1129
if index != 0:
1130
f.write(make_separator())
1131
1132
# Create enumeration signature and anchor point.
1133
1134
enum_anchor = f"enum_{sanitize_class_name(class_name)}_{e.name}"
1135
f.write(f".. _{enum_anchor}:\n\n")
1136
self_link = f":ref:`🔗<{enum_anchor}>`"
1137
f.write(".. rst-class:: classref-enumeration\n\n")
1138
1139
if e.is_bitfield:
1140
f.write(f"flags **{e.name}**: {self_link}\n\n")
1141
else:
1142
f.write(f"enum **{e.name}**: {self_link}\n\n")
1143
1144
for value in e.values.values():
1145
# Also create signature and anchor point for each enum constant.
1146
1147
f.write(f".. _class_{sanitize_class_name(class_name)}_constant_{value.name}:\n\n")
1148
f.write(".. rst-class:: classref-enumeration-constant\n\n")
1149
1150
f.write(f"{e.type_name.to_rst(state)} **{value.name}** = ``{value.value}``\n\n")
1151
1152
# Add enum constant description.
1153
1154
f.write(make_deprecated_experimental(value, state))
1155
1156
if value.text is not None and value.text.strip() != "":
1157
f.write(f"{format_text_block(value.text.strip(), value, state)}")
1158
elif value.deprecated is None and value.experimental is None:
1159
f.write(".. container:: contribute\n\n\t")
1160
f.write(
1161
translate(
1162
"There is currently no description for this enum. Please help us by `contributing one <https://contributing.godotengine.org/en/latest/documentation/class_reference.html>`__!"
1163
)
1164
+ "\n\n"
1165
)
1166
1167
f.write("\n\n")
1168
1169
index += 1
1170
1171
# Constant descriptions
1172
if len(class_def.constants) > 0:
1173
f.write(make_separator(True))
1174
f.write(".. rst-class:: classref-descriptions-group\n\n")
1175
f.write(make_heading("Constants", "-"))
1176
1177
for constant in class_def.constants.values():
1178
# Create constant signature and anchor point.
1179
1180
constant_anchor = f"class_{sanitize_class_name(class_name)}_constant_{constant.name}"
1181
f.write(f".. _{constant_anchor}:\n\n")
1182
self_link = f":ref:`🔗<{constant_anchor}>`"
1183
f.write(".. rst-class:: classref-constant\n\n")
1184
1185
f.write(f"**{constant.name}** = ``{constant.value}`` {self_link}\n\n")
1186
1187
# Add constant description.
1188
1189
f.write(make_deprecated_experimental(constant, state))
1190
1191
if constant.text is not None and constant.text.strip() != "":
1192
f.write(f"{format_text_block(constant.text.strip(), constant, state)}")
1193
elif constant.deprecated is None and constant.experimental is None:
1194
f.write(".. container:: contribute\n\n\t")
1195
f.write(
1196
translate(
1197
"There is currently no description for this constant. Please help us by `contributing one <https://contributing.godotengine.org/en/latest/documentation/class_reference.html>`__!"
1198
)
1199
+ "\n\n"
1200
)
1201
1202
f.write("\n\n")
1203
1204
# Annotation descriptions
1205
if len(class_def.annotations) > 0:
1206
f.write(make_separator(True))
1207
f.write(".. rst-class:: classref-descriptions-group\n\n")
1208
f.write(make_heading("Annotations", "-"))
1209
1210
index = 0
1211
1212
for method_list in class_def.annotations.values(): # type: ignore
1213
for i, m in enumerate(method_list):
1214
if index != 0:
1215
f.write(make_separator())
1216
1217
# Create annotation signature and anchor point.
1218
1219
self_link = ""
1220
if i == 0:
1221
annotation_anchor = f"class_{sanitize_class_name(class_name)}_annotation_{m.name}"
1222
f.write(f".. _{annotation_anchor}:\n\n")
1223
self_link = f" :ref:`🔗<{annotation_anchor}>`"
1224
1225
f.write(".. rst-class:: classref-annotation\n\n")
1226
1227
_, signature = make_method_signature(class_def, m, "", state)
1228
f.write(f"{signature}{self_link}\n\n")
1229
1230
# Add annotation description, or a call to action if it's missing.
1231
1232
if m.description is not None and m.description.strip() != "":
1233
f.write(f"{format_text_block(m.description.strip(), m, state)}\n\n")
1234
else:
1235
f.write(".. container:: contribute\n\n\t")
1236
f.write(
1237
translate(
1238
"There is currently no description for this annotation. Please help us by `contributing one <https://contributing.godotengine.org/en/latest/documentation/class_reference.html>`__!"
1239
)
1240
+ "\n\n"
1241
)
1242
1243
index += 1
1244
1245
# Property descriptions
1246
if any(not p.overrides for p in class_def.properties.values()) > 0:
1247
f.write(make_separator(True))
1248
f.write(".. rst-class:: classref-descriptions-group\n\n")
1249
f.write(make_heading("Property Descriptions", "-"))
1250
1251
index = 0
1252
1253
for property_def in class_def.properties.values():
1254
if property_def.overrides:
1255
continue
1256
1257
if index != 0:
1258
f.write(make_separator())
1259
1260
# Create property signature and anchor point.
1261
1262
property_anchor = f"class_{sanitize_class_name(class_name)}_property_{property_def.name}"
1263
f.write(f".. _{property_anchor}:\n\n")
1264
self_link = f":ref:`🔗<{property_anchor}>`"
1265
f.write(".. rst-class:: classref-property\n\n")
1266
1267
property_default = ""
1268
if property_def.default_value is not None:
1269
property_default = f" = {property_def.default_value}"
1270
f.write(
1271
f"{property_def.type_name.to_rst(state)} **{property_def.name}**{property_default} {self_link}\n\n"
1272
)
1273
1274
# Create property setter and getter records.
1275
1276
property_setget = ""
1277
1278
if property_def.setter is not None and not property_def.setter.startswith("_"):
1279
property_setter = make_setter_signature(class_def, property_def, state)
1280
property_setget += f"- {property_setter}\n"
1281
1282
if property_def.getter is not None and not property_def.getter.startswith("_"):
1283
property_getter = make_getter_signature(class_def, property_def, state)
1284
property_setget += f"- {property_getter}\n"
1285
1286
if property_setget != "":
1287
f.write(".. rst-class:: classref-property-setget\n\n")
1288
f.write(property_setget)
1289
f.write("\n")
1290
1291
# Add property description, or a call to action if it's missing.
1292
1293
f.write(make_deprecated_experimental(property_def, state))
1294
1295
if property_def.text is not None and property_def.text.strip() != "":
1296
f.write(f"{format_text_block(property_def.text.strip(), property_def, state)}\n\n")
1297
elif property_def.deprecated is None and property_def.experimental is None:
1298
f.write(".. container:: contribute\n\n\t")
1299
f.write(
1300
translate(
1301
"There is currently no description for this property. Please help us by `contributing one <https://contributing.godotengine.org/en/latest/documentation/class_reference.html>`__!"
1302
)
1303
+ "\n\n"
1304
)
1305
1306
# Add copy note to built-in properties returning `Packed*Array`.
1307
if property_def.type_name.type_name in PACKED_ARRAY_TYPES:
1308
# See also `BASE_STRINGS` and `editor/doc/editor_help.cpp`.
1309
copy_note = f"[b]Note:[/b] The returned array is [i]copied[/i] and any changes to it will not update the original property value. See [{property_def.type_name.type_name}] for more details."
1310
f.write(f"{format_text_block(copy_note, property_def, state)}\n\n")
1311
1312
index += 1
1313
1314
# Constructor, Method, Operator descriptions
1315
if len(class_def.constructors) > 0:
1316
f.write(make_separator(True))
1317
f.write(".. rst-class:: classref-descriptions-group\n\n")
1318
f.write(make_heading("Constructor Descriptions", "-"))
1319
1320
index = 0
1321
1322
for method_list in class_def.constructors.values():
1323
for i, m in enumerate(method_list):
1324
if index != 0:
1325
f.write(make_separator())
1326
1327
# Create constructor signature and anchor point.
1328
1329
self_link = ""
1330
if i == 0:
1331
constructor_anchor = f"class_{sanitize_class_name(class_name)}_constructor_{m.name}"
1332
f.write(f".. _{constructor_anchor}:\n\n")
1333
self_link = f" :ref:`🔗<{constructor_anchor}>`"
1334
1335
f.write(".. rst-class:: classref-constructor\n\n")
1336
1337
ret_type, signature = make_method_signature(class_def, m, "", state)
1338
f.write(f"{ret_type} {signature}{self_link}\n\n")
1339
1340
# Add constructor description, or a call to action if it's missing.
1341
1342
f.write(make_deprecated_experimental(m, state))
1343
1344
if m.description is not None and m.description.strip() != "":
1345
f.write(f"{format_text_block(m.description.strip(), m, state)}\n\n")
1346
elif m.deprecated is None and m.experimental is None:
1347
f.write(".. container:: contribute\n\n\t")
1348
f.write(
1349
translate(
1350
"There is currently no description for this constructor. Please help us by `contributing one <https://contributing.godotengine.org/en/latest/documentation/class_reference.html>`__!"
1351
)
1352
+ "\n\n"
1353
)
1354
1355
index += 1
1356
1357
if len(class_def.methods) > 0:
1358
f.write(make_separator(True))
1359
f.write(".. rst-class:: classref-descriptions-group\n\n")
1360
f.write(make_heading("Method Descriptions", "-"))
1361
1362
index = 0
1363
1364
for method_list in class_def.methods.values():
1365
for i, m in enumerate(method_list):
1366
if index != 0:
1367
f.write(make_separator())
1368
1369
# Create method signature and anchor point.
1370
1371
self_link = ""
1372
1373
if i == 0:
1374
method_qualifier = ""
1375
if m.name.startswith("_"):
1376
method_qualifier = "private_"
1377
method_anchor = f"class_{sanitize_class_name(class_name)}_{method_qualifier}method_{m.name}"
1378
f.write(f".. _{method_anchor}:\n\n")
1379
self_link = f" :ref:`🔗<{method_anchor}>`"
1380
1381
f.write(".. rst-class:: classref-method\n\n")
1382
1383
ret_type, signature = make_method_signature(class_def, m, "", state)
1384
1385
f.write(f"{ret_type} {signature}{self_link}\n\n")
1386
1387
# Add method description, or a call to action if it's missing.
1388
1389
f.write(make_deprecated_experimental(m, state))
1390
1391
if m.description is not None and m.description.strip() != "":
1392
f.write(f"{format_text_block(m.description.strip(), m, state)}\n\n")
1393
elif m.deprecated is None and m.experimental is None:
1394
f.write(".. container:: contribute\n\n\t")
1395
f.write(
1396
translate(
1397
"There is currently no description for this method. Please help us by `contributing one <https://contributing.godotengine.org/en/latest/documentation/class_reference.html>`__!"
1398
)
1399
+ "\n\n"
1400
)
1401
1402
index += 1
1403
1404
if len(class_def.operators) > 0:
1405
f.write(make_separator(True))
1406
f.write(".. rst-class:: classref-descriptions-group\n\n")
1407
f.write(make_heading("Operator Descriptions", "-"))
1408
1409
index = 0
1410
1411
for method_list in class_def.operators.values():
1412
for i, m in enumerate(method_list):
1413
if index != 0:
1414
f.write(make_separator())
1415
1416
# Create operator signature and anchor point.
1417
1418
operator_anchor = (
1419
f"class_{sanitize_class_name(class_name)}_operator_{sanitize_operator_name(m.name, state)}"
1420
)
1421
for parameter in m.parameters:
1422
operator_anchor += f"_{parameter.type_name.type_name}"
1423
f.write(f".. _{operator_anchor}:\n\n")
1424
self_link = f":ref:`🔗<{operator_anchor}>`"
1425
1426
f.write(".. rst-class:: classref-operator\n\n")
1427
1428
ret_type, signature = make_method_signature(class_def, m, "", state)
1429
f.write(f"{ret_type} {signature} {self_link}\n\n")
1430
1431
# Add operator description, or a call to action if it's missing.
1432
1433
f.write(make_deprecated_experimental(m, state))
1434
1435
if m.description is not None and m.description.strip() != "":
1436
f.write(f"{format_text_block(m.description.strip(), m, state)}\n\n")
1437
elif m.deprecated is None and m.experimental is None:
1438
f.write(".. container:: contribute\n\n\t")
1439
f.write(
1440
translate(
1441
"There is currently no description for this operator. Please help us by `contributing one <https://contributing.godotengine.org/en/latest/documentation/class_reference.html>`__!"
1442
)
1443
+ "\n\n"
1444
)
1445
1446
index += 1
1447
1448
# Theme property descriptions
1449
if len(class_def.theme_items) > 0:
1450
f.write(make_separator(True))
1451
f.write(".. rst-class:: classref-descriptions-group\n\n")
1452
f.write(make_heading("Theme Property Descriptions", "-"))
1453
1454
index = 0
1455
1456
for theme_item_def in class_def.theme_items.values():
1457
if index != 0:
1458
f.write(make_separator())
1459
1460
# Create theme property signature and anchor point.
1461
1462
theme_item_anchor = (
1463
f"class_{sanitize_class_name(class_name)}_theme_{theme_item_def.data_name}_{theme_item_def.name}"
1464
)
1465
f.write(f".. _{theme_item_anchor}:\n\n")
1466
self_link = f":ref:`🔗<{theme_item_anchor}>`"
1467
f.write(".. rst-class:: classref-themeproperty\n\n")
1468
1469
theme_item_default = ""
1470
if theme_item_def.default_value is not None:
1471
theme_item_default = f" = {theme_item_def.default_value}"
1472
f.write(
1473
f"{theme_item_def.type_name.to_rst(state)} **{theme_item_def.name}**{theme_item_default} {self_link}\n\n"
1474
)
1475
1476
# Add theme property description, or a call to action if it's missing.
1477
1478
f.write(make_deprecated_experimental(theme_item_def, state))
1479
1480
if theme_item_def.text is not None and theme_item_def.text.strip() != "":
1481
f.write(f"{format_text_block(theme_item_def.text.strip(), theme_item_def, state)}\n\n")
1482
elif theme_item_def.deprecated is None and theme_item_def.experimental is None:
1483
f.write(".. container:: contribute\n\n\t")
1484
f.write(
1485
translate(
1486
"There is currently no description for this theme property. Please help us by `contributing one <https://contributing.godotengine.org/en/latest/documentation/class_reference.html>`__!"
1487
)
1488
+ "\n\n"
1489
)
1490
1491
index += 1
1492
1493
f.write(make_footer())
1494
1495
1496
def make_type(klass: str, state: State) -> str:
1497
if klass.find("*") != -1: # Pointer, ignore
1498
return f"``{klass}``"
1499
1500
def resolve_type(link_type: str) -> str:
1501
if link_type in state.classes:
1502
return f":ref:`{link_type}<class_{sanitize_class_name(link_type)}>`"
1503
else:
1504
print_error(f'{state.current_class}.xml: Unresolved type "{link_type}".', state)
1505
return f"``{link_type}``"
1506
1507
if klass.endswith("[]"): # Typed array, strip [] to link to contained type.
1508
return f":ref:`Array<class_Array>`\\[{resolve_type(klass[: -len('[]')])}\\]"
1509
1510
if klass.startswith("Dictionary["): # Typed dictionary, split elements to link contained types.
1511
parts = klass[len("Dictionary[") : -len("]")].partition(", ")
1512
key = parts[0]
1513
value = parts[2]
1514
return f":ref:`Dictionary<class_Dictionary>`\\[{resolve_type(key)}, {resolve_type(value)}\\]"
1515
1516
return resolve_type(klass)
1517
1518
1519
def make_enum(t: str, is_bitfield: bool, state: State) -> str:
1520
p = t.rfind(".")
1521
if p >= 0:
1522
c = t[0:p]
1523
e = t[p + 1 :]
1524
# Variant enums live in GlobalScope but still use periods.
1525
if c == "Variant":
1526
c = "@GlobalScope"
1527
e = "Variant." + e
1528
else:
1529
c = state.current_class
1530
e = t
1531
if c in state.classes and e not in state.classes[c].enums:
1532
c = "@GlobalScope"
1533
1534
if c in state.classes and e in state.classes[c].enums:
1535
if is_bitfield:
1536
if not state.classes[c].enums[e].is_bitfield:
1537
print_error(f'{state.current_class}.xml: Enum "{t}" is not bitfield.', state)
1538
return f"|bitfield|\\[:ref:`{e}<enum_{sanitize_class_name(c)}_{e}>`\\]"
1539
else:
1540
return f":ref:`{e}<enum_{sanitize_class_name(c)}_{e}>`"
1541
1542
print_error(f'{state.current_class}.xml: Unresolved enum "{t}".', state)
1543
1544
return t
1545
1546
1547
def make_method_signature(
1548
class_def: ClassDef, definition: AnnotationDef | MethodDef | SignalDef, ref_type: str, state: State
1549
) -> tuple[str, str]:
1550
ret_type = ""
1551
1552
if isinstance(definition, MethodDef):
1553
ret_type = definition.return_type.to_rst(state)
1554
1555
qualifiers = None
1556
if isinstance(definition, (MethodDef, AnnotationDef)):
1557
qualifiers = definition.qualifiers
1558
1559
out = ""
1560
if isinstance(definition, MethodDef) and ref_type != "":
1561
if ref_type == "operator":
1562
op_name = definition.name.replace("<", "\\<") # So operator "<" gets correctly displayed.
1563
out += f":ref:`{op_name}<class_{sanitize_class_name(class_def.name)}_{ref_type}_{sanitize_operator_name(definition.name, state)}"
1564
for parameter in definition.parameters:
1565
out += f"_{parameter.type_name.type_name}"
1566
out += ">`"
1567
elif ref_type == "method":
1568
ref_type_qualifier = ""
1569
if definition.name.startswith("_"):
1570
ref_type_qualifier = "private_"
1571
out += f":ref:`{definition.name}<class_{sanitize_class_name(class_def.name)}_{ref_type_qualifier}{ref_type}_{definition.name}>`"
1572
else:
1573
out += f":ref:`{definition.name}<class_{sanitize_class_name(class_def.name)}_{ref_type}_{definition.name}>`"
1574
else:
1575
out += f"**{definition.name}**"
1576
1577
out += "\\ ("
1578
for i, arg in enumerate(definition.parameters):
1579
if i > 0:
1580
out += ", "
1581
else:
1582
out += "\\ "
1583
1584
out += f"{arg.name}\\: {arg.type_name.to_rst(state)}"
1585
1586
if arg.default_value is not None:
1587
out += f" = {arg.default_value}"
1588
1589
if qualifiers is not None and "vararg" in qualifiers:
1590
if len(definition.parameters) > 0:
1591
out += ", ..."
1592
else:
1593
out += "\\ ..."
1594
1595
out += "\\ )"
1596
1597
if qualifiers is not None:
1598
# Use substitutions for abbreviations. This is used to display tooltips on hover.
1599
# See `make_footer()` for descriptions.
1600
for qualifier in qualifiers.split():
1601
out += f" |{qualifier}|"
1602
1603
return ret_type, out
1604
1605
1606
def make_setter_signature(class_def: ClassDef, property_def: PropertyDef, state: State) -> str:
1607
if property_def.setter is None:
1608
return ""
1609
1610
# If setter is a method available as a method definition, we use that.
1611
if property_def.setter in class_def.methods:
1612
setter = class_def.methods[property_def.setter][0]
1613
# Otherwise we fake it with the information we have available.
1614
else:
1615
setter_params: list[ParameterDef] = []
1616
setter_params.append(ParameterDef("value", property_def.type_name, None))
1617
setter = MethodDef(property_def.setter, TypeName("void"), setter_params, None, None)
1618
1619
ret_type, signature = make_method_signature(class_def, setter, "", state)
1620
return f"{ret_type} {signature}"
1621
1622
1623
def make_getter_signature(class_def: ClassDef, property_def: PropertyDef, state: State) -> str:
1624
if property_def.getter is None:
1625
return ""
1626
1627
# If getter is a method available as a method definition, we use that.
1628
if property_def.getter in class_def.methods:
1629
getter = class_def.methods[property_def.getter][0]
1630
# Otherwise we fake it with the information we have available.
1631
else:
1632
getter_params: list[ParameterDef] = []
1633
getter = MethodDef(property_def.getter, property_def.type_name, getter_params, None, None)
1634
1635
ret_type, signature = make_method_signature(class_def, getter, "", state)
1636
return f"{ret_type} {signature}"
1637
1638
1639
def make_deprecated_experimental(item: DefinitionBase, state: State) -> str:
1640
result = ""
1641
1642
if item.deprecated is not None:
1643
deprecated_prefix = translate("Deprecated:")
1644
if item.deprecated.strip() == "":
1645
default_message = translate(f"This {item.definition_name} may be changed or removed in future versions.")
1646
result += f"**{deprecated_prefix}** {default_message}\n\n"
1647
else:
1648
result += f"**{deprecated_prefix}** {format_text_block(item.deprecated.strip(), item, state)}\n\n"
1649
1650
if item.experimental is not None:
1651
experimental_prefix = translate("Experimental:")
1652
if item.experimental.strip() == "":
1653
default_message = translate(f"This {item.definition_name} may be changed or removed in future versions.")
1654
result += f"**{experimental_prefix}** {default_message}\n\n"
1655
else:
1656
result += f"**{experimental_prefix}** {format_text_block(item.experimental.strip(), item, state)}\n\n"
1657
1658
return result
1659
1660
1661
def make_heading(title: str, underline: str, l10n: bool = True) -> str:
1662
if l10n:
1663
new_title = translate(title)
1664
if new_title != title:
1665
title = new_title
1666
underline *= 2 # Double length to handle wide chars.
1667
return f"{title}\n{(underline * len(title))}\n\n"
1668
1669
1670
def make_footer() -> str:
1671
# Generate reusable abbreviation substitutions.
1672
# This way, we avoid bloating the generated rST with duplicate abbreviations.
1673
virtual_msg = translate("This method should typically be overridden by the user to have any effect.")
1674
required_msg = translate("This method is required to be overridden when extending its base class.")
1675
const_msg = translate("This method has no side effects. It doesn't modify any of the instance's member variables.")
1676
vararg_msg = translate("This method accepts any number of arguments after the ones described here.")
1677
constructor_msg = translate("This method is used to construct a type.")
1678
static_msg = translate(
1679
"This method doesn't need an instance to be called, so it can be called directly using the class name."
1680
)
1681
operator_msg = translate("This method describes a valid operator to use with this type as left-hand operand.")
1682
bitfield_msg = translate("This value is an integer composed as a bitmask of the following flags.")
1683
void_msg = translate("No return value.")
1684
1685
return (
1686
f".. |virtual| replace:: :abbr:`virtual ({virtual_msg})`\n"
1687
f".. |required| replace:: :abbr:`required ({required_msg})`\n"
1688
f".. |const| replace:: :abbr:`const ({const_msg})`\n"
1689
f".. |vararg| replace:: :abbr:`vararg ({vararg_msg})`\n"
1690
f".. |constructor| replace:: :abbr:`constructor ({constructor_msg})`\n"
1691
f".. |static| replace:: :abbr:`static ({static_msg})`\n"
1692
f".. |operator| replace:: :abbr:`operator ({operator_msg})`\n"
1693
f".. |bitfield| replace:: :abbr:`BitField ({bitfield_msg})`\n"
1694
f".. |void| replace:: :abbr:`void ({void_msg})`\n"
1695
)
1696
1697
1698
def make_separator(section_level: bool = False) -> str:
1699
separator_class = "item"
1700
if section_level:
1701
separator_class = "section"
1702
1703
return f".. rst-class:: classref-{separator_class}-separator\n\n----\n\n"
1704
1705
1706
def make_link(url: str, title: str) -> str:
1707
match = GODOT_DOCS_PATTERN.search(url)
1708
if match:
1709
groups = match.groups()
1710
if match.lastindex == 2:
1711
# Doc reference with fragment identifier: emit direct link to section with reference to page, for example:
1712
# `#calling-javascript-from-script in Exporting For Web`
1713
# Or use the title if provided.
1714
if title != "":
1715
return f"`{title} <../{groups[0]}.html{groups[1]}>`__"
1716
return f"`{groups[1]} <../{groups[0]}.html{groups[1]}>`__ in :doc:`../{groups[0]}`"
1717
elif match.lastindex == 1:
1718
# Doc reference, for example:
1719
# `Math`
1720
if title != "":
1721
return f":doc:`{title} <../{groups[0]}>`"
1722
return f":doc:`../{groups[0]}`"
1723
1724
# External link, for example:
1725
# `http://enet.bespin.org/usergroup0.html`
1726
if title != "":
1727
return f"`{title} <{url}>`__"
1728
return f"`{url} <{url}>`__"
1729
1730
1731
def make_rst_index(grouped_classes: dict[str, list[str]], dry_run: bool, output_dir: str) -> None:
1732
with open(
1733
os.devnull if dry_run else os.path.join(output_dir, "index.rst"), "w", encoding="utf-8", newline="\n"
1734
) as f:
1735
# Remove the "Edit on Github" button from the online docs page, and disallow user-contributed notes
1736
# on the index page. User-contributed notes are allowed on individual class pages.
1737
f.write(":github_url: hide\n:allow_comments: False\n\n")
1738
1739
# Warn contributors not to edit this file directly.
1740
# Also provide links to the source files for reference.
1741
1742
git_branch = get_git_branch()
1743
generator_github_url = f"https://github.com/godotengine/godot/tree/{git_branch}/doc/tools/make_rst.py"
1744
1745
f.write(".. DO NOT EDIT THIS FILE!!!\n")
1746
f.write(".. Generated automatically from Godot engine sources.\n")
1747
f.write(f".. Generator: {generator_github_url}.\n\n")
1748
1749
f.write(".. _doc_class_reference:\n\n")
1750
1751
f.write(make_heading("All classes", "="))
1752
1753
for group_name in CLASS_GROUPS:
1754
if group_name in grouped_classes:
1755
f.write(make_heading(CLASS_GROUPS[group_name], "="))
1756
1757
f.write(".. toctree::\n")
1758
f.write(" :maxdepth: 1\n")
1759
f.write(f" :name: toc-class-ref-{group_name}s\n")
1760
f.write("\n")
1761
1762
if group_name in CLASS_GROUPS_BASE:
1763
f.write(f" class_{sanitize_class_name(CLASS_GROUPS_BASE[group_name], True)}\n")
1764
1765
for class_name in grouped_classes[group_name]:
1766
if group_name in CLASS_GROUPS_BASE and sanitize_class_name(
1767
CLASS_GROUPS_BASE[group_name], True
1768
) == sanitize_class_name(class_name, True):
1769
continue
1770
1771
f.write(f" class_{sanitize_class_name(class_name, True)}\n")
1772
1773
f.write("\n")
1774
1775
1776
# Formatting helpers.
1777
1778
1779
RESERVED_FORMATTING_TAGS = ["i", "b", "u", "lb", "rb", "code", "kbd", "center", "url", "br"]
1780
RESERVED_LAYOUT_TAGS = ["codeblocks"]
1781
RESERVED_CODEBLOCK_TAGS = ["codeblock", "gdscript", "csharp"]
1782
RESERVED_CROSSLINK_TAGS = [
1783
"method",
1784
"constructor",
1785
"operator",
1786
"member",
1787
"signal",
1788
"constant",
1789
"enum",
1790
"annotation",
1791
"theme_item",
1792
"param",
1793
]
1794
1795
1796
def is_in_tagset(tag_text: str, tagset: list[str]) -> bool:
1797
for tag in tagset:
1798
# Complete match.
1799
if tag_text == tag:
1800
return True
1801
# Tag with arguments.
1802
if tag_text.startswith(tag + " "):
1803
return True
1804
# Tag with arguments, special case for [url], [color], and [font].
1805
if tag_text.startswith(tag + "="):
1806
return True
1807
1808
return False
1809
1810
1811
def get_tag_and_args(tag_text: str) -> TagState:
1812
tag_name = tag_text
1813
arguments: str = ""
1814
1815
delim_pos = -1
1816
1817
space_pos = tag_text.find(" ")
1818
if space_pos >= 0:
1819
delim_pos = space_pos
1820
1821
# Special case for [url], [color], and [font].
1822
assign_pos = tag_text.find("=")
1823
if assign_pos >= 0 and (delim_pos < 0 or assign_pos < delim_pos):
1824
delim_pos = assign_pos
1825
1826
if delim_pos >= 0:
1827
tag_name = tag_text[:delim_pos]
1828
arguments = tag_text[delim_pos + 1 :].strip()
1829
1830
closing = False
1831
if tag_name.startswith("/"):
1832
tag_name = tag_name[1:]
1833
closing = True
1834
1835
return TagState(tag_text, tag_name, arguments, closing)
1836
1837
1838
def parse_link_target(link_target: str, state: State, context_name: str) -> list[str]:
1839
if link_target.find(".") != -1:
1840
return link_target.split(".")
1841
else:
1842
return [state.current_class, link_target]
1843
1844
1845
def format_text_block(
1846
text: str,
1847
context: DefinitionBase,
1848
state: State,
1849
) -> str:
1850
result = preformat_text_block(text, state)
1851
if result is None:
1852
return ""
1853
text = result
1854
1855
next_brac_pos = text.find("[")
1856
text = escape_rst(text, next_brac_pos)
1857
1858
context_name = format_context_name(context)
1859
1860
# Handle [tags]
1861
inside_code = False
1862
inside_code_tag = ""
1863
inside_code_tabs = False
1864
ignore_code_warnings = False
1865
code_warning_if_intended_string = "If this is intended, use [code skip-lint]...[/code]."
1866
1867
has_codeblocks_gdscript = False
1868
has_codeblocks_csharp = False
1869
1870
pos = 0
1871
tag_depth = 0
1872
while True:
1873
pos = text.find("[", pos)
1874
if pos == -1:
1875
break
1876
1877
endq_pos = text.find("]", pos + 1)
1878
if endq_pos == -1:
1879
break
1880
1881
pre_text = text[:pos]
1882
post_text = text[endq_pos + 1 :]
1883
tag_text = text[pos + 1 : endq_pos]
1884
1885
escape_pre = False
1886
escape_post = False
1887
1888
# Tag is a reference to a class.
1889
if tag_text in state.classes and not inside_code:
1890
if tag_text == state.current_class:
1891
# Don't create a link to the same class, format it as strong emphasis.
1892
tag_text = f"**{tag_text}**"
1893
else:
1894
tag_text = make_type(tag_text, state)
1895
escape_pre = True
1896
escape_post = True
1897
1898
# Tag is a cross-reference or a formatting directive.
1899
else:
1900
tag_state = get_tag_and_args(tag_text)
1901
1902
# Anything identified as a tag inside of a code block is valid,
1903
# unless it's a matching closing tag.
1904
if inside_code:
1905
# Exiting codeblocks and inline code tags.
1906
1907
if tag_state.closing and tag_state.name == inside_code_tag:
1908
if is_in_tagset(tag_state.name, RESERVED_CODEBLOCK_TAGS):
1909
tag_text = ""
1910
tag_depth -= 1
1911
inside_code = False
1912
ignore_code_warnings = False
1913
# Strip newline if the tag was alone on one
1914
if pre_text[-1] == "\n":
1915
pre_text = pre_text[:-1]
1916
1917
elif is_in_tagset(tag_state.name, ["code"]):
1918
tag_text = "``"
1919
tag_depth -= 1
1920
inside_code = False
1921
ignore_code_warnings = False
1922
escape_post = True
1923
1924
else:
1925
if not ignore_code_warnings and tag_state.closing:
1926
print_warning(
1927
f'{state.current_class}.xml: Found a code string that looks like a closing tag "[{tag_state.raw}]" in {context_name}. {code_warning_if_intended_string}',
1928
state,
1929
)
1930
1931
tag_text = f"[{tag_text}]"
1932
1933
# Entering codeblocks and inline code tags.
1934
1935
elif tag_state.name == "codeblocks":
1936
if tag_state.closing:
1937
if not has_codeblocks_gdscript or not has_codeblocks_csharp:
1938
state.script_language_parity_check.add_hit(
1939
state.current_class,
1940
context,
1941
"Only one script language sample found in [codeblocks]",
1942
state,
1943
)
1944
1945
has_codeblocks_gdscript = False
1946
has_codeblocks_csharp = False
1947
1948
tag_depth -= 1
1949
tag_text = ""
1950
inside_code_tabs = False
1951
else:
1952
tag_depth += 1
1953
tag_text = "\n.. tabs::"
1954
inside_code_tabs = True
1955
1956
elif is_in_tagset(tag_state.name, RESERVED_CODEBLOCK_TAGS):
1957
tag_depth += 1
1958
1959
if tag_state.name == "gdscript":
1960
if not inside_code_tabs:
1961
print_error(
1962
f"{state.current_class}.xml: GDScript code block is used outside of [codeblocks] in {context_name}.",
1963
state,
1964
)
1965
else:
1966
has_codeblocks_gdscript = True
1967
tag_text = "\n .. code-tab:: gdscript\n"
1968
elif tag_state.name == "csharp":
1969
if not inside_code_tabs:
1970
print_error(
1971
f"{state.current_class}.xml: C# code block is used outside of [codeblocks] in {context_name}.",
1972
state,
1973
)
1974
else:
1975
has_codeblocks_csharp = True
1976
tag_text = "\n .. code-tab:: csharp\n"
1977
else:
1978
state.script_language_parity_check.add_hit(
1979
state.current_class,
1980
context,
1981
"Code sample is formatted with [codeblock] where [codeblocks] should be used",
1982
state,
1983
)
1984
1985
if "lang=text" in tag_state.arguments.split(" "):
1986
tag_text = "\n.. code:: text\n"
1987
else:
1988
tag_text = "\n::\n"
1989
1990
inside_code = True
1991
inside_code_tag = tag_state.name
1992
ignore_code_warnings = "skip-lint" in tag_state.arguments.split(" ")
1993
1994
elif is_in_tagset(tag_state.name, ["code"]):
1995
tag_text = "``"
1996
tag_depth += 1
1997
1998
inside_code = True
1999
inside_code_tag = "code"
2000
ignore_code_warnings = "skip-lint" in tag_state.arguments.split(" ")
2001
escape_pre = True
2002
2003
if not ignore_code_warnings:
2004
endcode_pos = text.find("[/code]", endq_pos + 1)
2005
if endcode_pos == -1:
2006
print_error(
2007
f"{state.current_class}.xml: Tag depth mismatch for [code]: no closing [/code] in {context_name}.",
2008
state,
2009
)
2010
break
2011
2012
inside_code_text = text[endq_pos + 1 : endcode_pos]
2013
if inside_code_text.endswith("()"):
2014
# It's formatted like a call for some reason, may still be a mistake.
2015
inside_code_text = inside_code_text[:-2]
2016
2017
if inside_code_text in state.classes:
2018
print_warning(
2019
f'{state.current_class}.xml: Found a code string "{inside_code_text}" that matches one of the known classes in {context_name}. {code_warning_if_intended_string}',
2020
state,
2021
)
2022
2023
target_class_name, target_name, *rest = parse_link_target(inside_code_text, state, context_name)
2024
if len(rest) == 0 and target_class_name in state.classes:
2025
class_def = state.classes[target_class_name]
2026
2027
if target_name in class_def.methods:
2028
print_warning(
2029
f'{state.current_class}.xml: Found a code string "{inside_code_text}" that matches the {target_class_name}.{target_name} method in {context_name}. {code_warning_if_intended_string}',
2030
state,
2031
)
2032
2033
elif target_name in class_def.constructors:
2034
print_warning(
2035
f'{state.current_class}.xml: Found a code string "{inside_code_text}" that matches the {target_class_name}.{target_name} constructor in {context_name}. {code_warning_if_intended_string}',
2036
state,
2037
)
2038
2039
elif target_name in class_def.operators:
2040
print_warning(
2041
f'{state.current_class}.xml: Found a code string "{inside_code_text}" that matches the {target_class_name}.{target_name} operator in {context_name}. {code_warning_if_intended_string}',
2042
state,
2043
)
2044
2045
elif target_name in class_def.properties:
2046
print_warning(
2047
f'{state.current_class}.xml: Found a code string "{inside_code_text}" that matches the {target_class_name}.{target_name} member in {context_name}. {code_warning_if_intended_string}',
2048
state,
2049
)
2050
2051
elif target_name in class_def.signals:
2052
print_warning(
2053
f'{state.current_class}.xml: Found a code string "{inside_code_text}" that matches the {target_class_name}.{target_name} signal in {context_name}. {code_warning_if_intended_string}',
2054
state,
2055
)
2056
2057
elif target_name in class_def.annotations:
2058
print_warning(
2059
f'{state.current_class}.xml: Found a code string "{inside_code_text}" that matches the {target_class_name}.{target_name} annotation in {context_name}. {code_warning_if_intended_string}',
2060
state,
2061
)
2062
2063
elif target_name in class_def.theme_items:
2064
print_warning(
2065
f'{state.current_class}.xml: Found a code string "{inside_code_text}" that matches the {target_class_name}.{target_name} theme property in {context_name}. {code_warning_if_intended_string}',
2066
state,
2067
)
2068
2069
elif target_name in class_def.constants:
2070
print_warning(
2071
f'{state.current_class}.xml: Found a code string "{inside_code_text}" that matches the {target_class_name}.{target_name} constant in {context_name}. {code_warning_if_intended_string}',
2072
state,
2073
)
2074
2075
else:
2076
for enum in class_def.enums.values():
2077
if target_name in enum.values:
2078
print_warning(
2079
f'{state.current_class}.xml: Found a code string "{inside_code_text}" that matches the {target_class_name}.{target_name} enum value in {context_name}. {code_warning_if_intended_string}',
2080
state,
2081
)
2082
break
2083
2084
valid_param_context = isinstance(context, (MethodDef, SignalDef, AnnotationDef))
2085
if valid_param_context:
2086
context_params: list[ParameterDef] = context.parameters # type: ignore
2087
for param_def in context_params:
2088
if param_def.name == inside_code_text:
2089
print_warning(
2090
f'{state.current_class}.xml: Found a code string "{inside_code_text}" that matches one of the parameters in {context_name}. {code_warning_if_intended_string}',
2091
state,
2092
)
2093
break
2094
2095
# Cross-references to items in this or other class documentation pages.
2096
elif is_in_tagset(tag_state.name, RESERVED_CROSSLINK_TAGS):
2097
link_target: str = tag_state.arguments
2098
2099
if link_target == "":
2100
print_error(
2101
f'{state.current_class}.xml: Empty cross-reference link "[{tag_state.raw}]" in {context_name}.',
2102
state,
2103
)
2104
tag_text = ""
2105
else:
2106
if (
2107
tag_state.name == "method"
2108
or tag_state.name == "constructor"
2109
or tag_state.name == "operator"
2110
or tag_state.name == "member"
2111
or tag_state.name == "signal"
2112
or tag_state.name == "annotation"
2113
or tag_state.name == "theme_item"
2114
or tag_state.name == "constant"
2115
):
2116
target_class_name, target_name, *rest = parse_link_target(link_target, state, context_name)
2117
if len(rest) > 0:
2118
print_error(
2119
f'{state.current_class}.xml: Bad reference "{link_target}" in {context_name}.',
2120
state,
2121
)
2122
2123
# Default to the tag command name. This works by default for most tags,
2124
# but method, member, and theme_item have special cases.
2125
ref_type = "_{}".format(tag_state.name)
2126
2127
if target_class_name in state.classes:
2128
class_def = state.classes[target_class_name]
2129
2130
if tag_state.name == "method":
2131
if target_name.startswith("_"):
2132
ref_type = "_private_method"
2133
2134
if target_name not in class_def.methods:
2135
print_error(
2136
f'{state.current_class}.xml: Unresolved method reference "{link_target}" in {context_name}.',
2137
state,
2138
)
2139
2140
elif tag_state.name == "constructor" and target_name not in class_def.constructors:
2141
print_error(
2142
f'{state.current_class}.xml: Unresolved constructor reference "{link_target}" in {context_name}.',
2143
state,
2144
)
2145
2146
elif tag_state.name == "operator" and target_name not in class_def.operators:
2147
print_error(
2148
f'{state.current_class}.xml: Unresolved operator reference "{link_target}" in {context_name}.',
2149
state,
2150
)
2151
2152
elif tag_state.name == "member":
2153
ref_type = "_property"
2154
2155
if target_name not in class_def.properties:
2156
print_error(
2157
f'{state.current_class}.xml: Unresolved member reference "{link_target}" in {context_name}.',
2158
state,
2159
)
2160
2161
elif class_def.properties[target_name].overrides is not None:
2162
print_error(
2163
f'{state.current_class}.xml: Invalid member reference "{link_target}" in {context_name}. The reference must point to the original definition, not to the override.',
2164
state,
2165
)
2166
2167
elif tag_state.name == "signal" and target_name not in class_def.signals:
2168
print_error(
2169
f'{state.current_class}.xml: Unresolved signal reference "{link_target}" in {context_name}.',
2170
state,
2171
)
2172
2173
elif tag_state.name == "annotation" and target_name not in class_def.annotations:
2174
print_error(
2175
f'{state.current_class}.xml: Unresolved annotation reference "{link_target}" in {context_name}.',
2176
state,
2177
)
2178
2179
elif tag_state.name == "theme_item":
2180
if target_name not in class_def.theme_items:
2181
print_error(
2182
f'{state.current_class}.xml: Unresolved theme property reference "{link_target}" in {context_name}.',
2183
state,
2184
)
2185
else:
2186
# Needs theme data type to be properly linked, which we cannot get without a class.
2187
name = class_def.theme_items[target_name].data_name
2188
ref_type = f"_theme_{name}"
2189
2190
elif tag_state.name == "constant":
2191
found = False
2192
2193
# Search in the current class
2194
search_class_defs = [class_def]
2195
2196
if link_target.find(".") == -1:
2197
# Also search in @GlobalScope as a last resort if no class was specified
2198
search_class_defs.append(state.classes["@GlobalScope"])
2199
2200
for search_class_def in search_class_defs:
2201
if target_name in search_class_def.constants:
2202
target_class_name = search_class_def.name
2203
found = True
2204
2205
else:
2206
for enum in search_class_def.enums.values():
2207
if target_name in enum.values:
2208
target_class_name = search_class_def.name
2209
found = True
2210
break
2211
2212
if not found:
2213
print_error(
2214
f'{state.current_class}.xml: Unresolved constant reference "{link_target}" in {context_name}.',
2215
state,
2216
)
2217
2218
else:
2219
print_error(
2220
f'{state.current_class}.xml: Unresolved type reference "{target_class_name}" in method reference "{link_target}" in {context_name}.',
2221
state,
2222
)
2223
2224
repl_text = target_name
2225
if target_class_name != state.current_class:
2226
repl_text = f"{target_class_name}.{target_name}"
2227
if tag_state.name == "method":
2228
repl_text = f"{repl_text}()"
2229
tag_text = f":ref:`{repl_text}<class_{sanitize_class_name(target_class_name)}{ref_type}_{target_name}>`"
2230
escape_pre = True
2231
escape_post = True
2232
2233
elif tag_state.name == "enum":
2234
tag_text = make_enum(link_target, False, state)
2235
escape_pre = True
2236
escape_post = True
2237
2238
elif tag_state.name == "param":
2239
valid_param_context = isinstance(context, (MethodDef, SignalDef, AnnotationDef))
2240
if not valid_param_context:
2241
print_error(
2242
f'{state.current_class}.xml: Argument reference "{link_target}" used outside of method, signal, or annotation context in {context_name}.',
2243
state,
2244
)
2245
else:
2246
context_params: list[ParameterDef] = context.parameters # type: ignore
2247
found = False
2248
for param_def in context_params:
2249
if param_def.name == link_target:
2250
found = True
2251
break
2252
if not found:
2253
print_error(
2254
f'{state.current_class}.xml: Unresolved argument reference "{link_target}" in {context_name}.',
2255
state,
2256
)
2257
2258
tag_text = f"``{link_target}``"
2259
escape_pre = True
2260
escape_post = True
2261
2262
# Formatting directives.
2263
2264
elif is_in_tagset(tag_state.name, ["url"]):
2265
url_target = tag_state.arguments
2266
2267
if url_target == "":
2268
print_error(
2269
f'{state.current_class}.xml: Misformatted [url] tag "[{tag_state.raw}]" in {context_name}.',
2270
state,
2271
)
2272
else:
2273
# Unlike other tags, URLs are handled in full here, as we need to extract
2274
# the optional link title to use `make_link`.
2275
endurl_pos = text.find("[/url]", endq_pos + 1)
2276
if endurl_pos == -1:
2277
print_error(
2278
f"{state.current_class}.xml: Tag depth mismatch for [url]: no closing [/url] in {context_name}.",
2279
state,
2280
)
2281
break
2282
link_title = text[endq_pos + 1 : endurl_pos]
2283
tag_text = make_link(url_target, link_title)
2284
2285
pre_text = text[:pos]
2286
post_text = text[endurl_pos + 6 :]
2287
2288
if pre_text and pre_text[-1] not in MARKUP_ALLOWED_PRECEDENT:
2289
pre_text += "\\ "
2290
if post_text and post_text[0] not in MARKUP_ALLOWED_SUBSEQUENT:
2291
post_text = "\\ " + post_text
2292
2293
text = pre_text + tag_text + post_text
2294
pos = len(pre_text) + len(tag_text)
2295
continue
2296
2297
elif tag_state.name == "br":
2298
# Make a new paragraph instead of a linebreak, rst is not so linebreak friendly
2299
tag_text = "\n\n"
2300
# Strip potential leading spaces
2301
while post_text[0] == " ":
2302
post_text = post_text[1:]
2303
2304
elif tag_state.name == "center":
2305
if tag_state.closing:
2306
tag_depth -= 1
2307
else:
2308
tag_depth += 1
2309
tag_text = ""
2310
2311
elif tag_state.name == "i":
2312
if tag_state.closing:
2313
tag_depth -= 1
2314
escape_post = True
2315
else:
2316
tag_depth += 1
2317
escape_pre = True
2318
tag_text = "*"
2319
2320
elif tag_state.name == "b":
2321
if tag_state.closing:
2322
tag_depth -= 1
2323
escape_post = True
2324
else:
2325
tag_depth += 1
2326
escape_pre = True
2327
tag_text = "**"
2328
2329
elif tag_state.name == "u":
2330
if tag_state.closing:
2331
tag_depth -= 1
2332
escape_post = True
2333
else:
2334
tag_depth += 1
2335
escape_pre = True
2336
tag_text = ""
2337
2338
elif tag_state.name == "lb":
2339
tag_text = "\\["
2340
2341
elif tag_state.name == "rb":
2342
tag_text = "\\]"
2343
2344
elif tag_state.name == "kbd":
2345
tag_text = "`"
2346
if tag_state.closing:
2347
tag_depth -= 1
2348
escape_post = True
2349
else:
2350
tag_text = ":kbd:" + tag_text
2351
tag_depth += 1
2352
escape_pre = True
2353
2354
# Invalid syntax.
2355
else:
2356
if tag_state.closing:
2357
print_error(
2358
f'{state.current_class}.xml: Unrecognized closing tag "[{tag_state.raw}]" in {context_name}.',
2359
state,
2360
)
2361
2362
tag_text = f"[{tag_text}]"
2363
else:
2364
print_error(
2365
f'{state.current_class}.xml: Unrecognized opening tag "[{tag_state.raw}]" in {context_name}.',
2366
state,
2367
)
2368
2369
tag_text = f"``{tag_text}``"
2370
escape_pre = True
2371
escape_post = True
2372
2373
# Properly escape things like `[Node]s`
2374
if escape_pre and pre_text and pre_text[-1] not in MARKUP_ALLOWED_PRECEDENT:
2375
pre_text += "\\ "
2376
if escape_post and post_text and post_text[0] not in MARKUP_ALLOWED_SUBSEQUENT:
2377
post_text = "\\ " + post_text
2378
2379
next_brac_pos = post_text.find("[", 0)
2380
iter_pos = 0
2381
while not inside_code:
2382
iter_pos = post_text.find("*", iter_pos, next_brac_pos)
2383
if iter_pos == -1:
2384
break
2385
post_text = f"{post_text[:iter_pos]}\\*{post_text[iter_pos + 1 :]}"
2386
iter_pos += 2
2387
2388
iter_pos = 0
2389
while not inside_code:
2390
iter_pos = post_text.find("_", iter_pos, next_brac_pos)
2391
if iter_pos == -1:
2392
break
2393
if not post_text[iter_pos + 1].isalnum(): # don't escape within a snake_case word
2394
post_text = f"{post_text[:iter_pos]}\\_{post_text[iter_pos + 1 :]}"
2395
iter_pos += 2
2396
else:
2397
iter_pos += 1
2398
2399
text = pre_text + tag_text + post_text
2400
pos = len(pre_text) + len(tag_text)
2401
2402
if tag_depth > 0:
2403
print_error(
2404
f"{state.current_class}.xml: Tag depth mismatch: too many (or too few) open/close tags in {context_name}.",
2405
state,
2406
)
2407
2408
return text
2409
2410
2411
def preformat_text_block(text: str, state: State) -> str | None:
2412
result = ""
2413
codeblock_tag = ""
2414
indent_level = 0
2415
2416
for line in text.splitlines():
2417
stripped_line = line.lstrip("\t")
2418
tab_count = len(line) - len(stripped_line)
2419
2420
if codeblock_tag:
2421
if line == "":
2422
result += "\n"
2423
continue
2424
2425
if tab_count < indent_level:
2426
print_error(f"{state.current_class}.xml: Invalid indentation.", state)
2427
return None
2428
2429
if stripped_line.startswith("[/" + codeblock_tag):
2430
result += stripped_line
2431
codeblock_tag = ""
2432
else:
2433
# Remove extraneous tabs and replace remaining tabs with spaces.
2434
result += "\n" + " " * (tab_count - indent_level + 1) + stripped_line
2435
else:
2436
if (
2437
stripped_line.startswith("[codeblock]")
2438
or stripped_line.startswith("[codeblock ")
2439
or stripped_line.startswith("[gdscript]")
2440
or stripped_line.startswith("[gdscript ")
2441
or stripped_line.startswith("[csharp]")
2442
or stripped_line.startswith("[csharp ")
2443
):
2444
if result:
2445
result += "\n"
2446
result += stripped_line
2447
2448
tag_text = stripped_line[1:].split("]", 1)[0]
2449
tag_state = get_tag_and_args(tag_text)
2450
codeblock_tag = tag_state.name
2451
indent_level = tab_count
2452
else:
2453
# A line break in XML should become two line breaks (unless in a code block).
2454
if result:
2455
result += "\n\n"
2456
result += stripped_line
2457
2458
return result
2459
2460
2461
def format_context_name(context: DefinitionBase | None) -> str:
2462
context_name: str = "unknown context"
2463
if context is not None:
2464
context_name = f'{context.definition_name} "{context.name}" description'
2465
2466
return context_name
2467
2468
2469
def escape_rst(text: str, until_pos: int = -1) -> str:
2470
# Escape \ character, otherwise it ends up as an escape character in rst
2471
pos = 0
2472
while True:
2473
pos = text.find("\\", pos, until_pos)
2474
if pos == -1:
2475
break
2476
text = f"{text[:pos]}\\\\{text[pos + 1 :]}"
2477
pos += 2
2478
2479
# Escape * character to avoid interpreting it as emphasis
2480
pos = 0
2481
while True:
2482
pos = text.find("*", pos, until_pos)
2483
if pos == -1:
2484
break
2485
text = f"{text[:pos]}\\*{text[pos + 1 :]}"
2486
pos += 2
2487
2488
# Escape _ character at the end of a word to avoid interpreting it as an inline hyperlink
2489
pos = 0
2490
while True:
2491
pos = text.find("_", pos, until_pos)
2492
if pos == -1:
2493
break
2494
if not text[pos + 1].isalnum(): # don't escape within a snake_case word
2495
text = f"{text[:pos]}\\_{text[pos + 1 :]}"
2496
pos += 2
2497
else:
2498
pos += 1
2499
2500
return text
2501
2502
2503
def format_table(f: TextIO, data: list[tuple[str | None, ...]], remove_empty_columns: bool = False) -> None:
2504
if len(data) == 0:
2505
return
2506
2507
f.write(".. table::\n")
2508
f.write(" :widths: auto\n\n")
2509
2510
# Calculate the width of each column first, we will use this information
2511
# to properly format RST-style tables.
2512
column_sizes = [0] * len(data[0])
2513
for row in data:
2514
for i, text in enumerate(row):
2515
text_length = len(text or "")
2516
if text_length > column_sizes[i]:
2517
column_sizes[i] = text_length
2518
2519
# Each table row is wrapped in two separators, consecutive rows share the same separator.
2520
# All separators, or rather borders, have the same shape and content. We compose it once,
2521
# then reuse it.
2522
2523
sep = ""
2524
for size in column_sizes:
2525
if size == 0 and remove_empty_columns:
2526
continue
2527
sep += "+" + "-" * (size + 2) # Content of each cell is padded by 1 on each side.
2528
sep += "+\n"
2529
2530
# Draw the first separator.
2531
f.write(f" {sep}")
2532
2533
# Draw each row and close it with a separator.
2534
for row in data:
2535
row_text = "|"
2536
for i, text in enumerate(row):
2537
if column_sizes[i] == 0 and remove_empty_columns:
2538
continue
2539
row_text += f" {(text or '').ljust(column_sizes[i])} |"
2540
row_text += "\n"
2541
2542
f.write(f" {row_text}")
2543
f.write(f" {sep}")
2544
2545
f.write("\n")
2546
2547
2548
def sanitize_class_name(dirty_name: str, is_file_name: bool = False) -> str:
2549
if is_file_name:
2550
return dirty_name.lower().replace('"', "").replace("/", "--")
2551
else:
2552
return dirty_name.replace('"', "").replace("/", "_").replace(".", "_")
2553
2554
2555
def sanitize_operator_name(dirty_name: str, state: State) -> str:
2556
clear_name = dirty_name.replace("operator ", "")
2557
2558
if clear_name == "!=":
2559
clear_name = "neq"
2560
elif clear_name == "==":
2561
clear_name = "eq"
2562
2563
elif clear_name == "<":
2564
clear_name = "lt"
2565
elif clear_name == "<=":
2566
clear_name = "lte"
2567
elif clear_name == ">":
2568
clear_name = "gt"
2569
elif clear_name == ">=":
2570
clear_name = "gte"
2571
2572
elif clear_name == "+":
2573
clear_name = "sum"
2574
elif clear_name == "-":
2575
clear_name = "dif"
2576
elif clear_name == "*":
2577
clear_name = "mul"
2578
elif clear_name == "/":
2579
clear_name = "div"
2580
elif clear_name == "%":
2581
clear_name = "mod"
2582
elif clear_name == "**":
2583
clear_name = "pow"
2584
2585
elif clear_name == "unary+":
2586
clear_name = "unplus"
2587
elif clear_name == "unary-":
2588
clear_name = "unminus"
2589
2590
elif clear_name == "<<":
2591
clear_name = "bwsl"
2592
elif clear_name == ">>":
2593
clear_name = "bwsr"
2594
elif clear_name == "&":
2595
clear_name = "bwand"
2596
elif clear_name == "|":
2597
clear_name = "bwor"
2598
elif clear_name == "^":
2599
clear_name = "bwxor"
2600
elif clear_name == "~":
2601
clear_name = "bwnot"
2602
2603
elif clear_name == "[]":
2604
clear_name = "idx"
2605
2606
else:
2607
clear_name = "xxx"
2608
print_error(f'Unsupported operator type "{dirty_name}", please add the missing rule.', state)
2609
2610
return clear_name
2611
2612
2613
if __name__ == "__main__":
2614
main()
2615
2616