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