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