1import sphinx 2 3from breathe.parser import compound, compoundsuper, DoxygenCompoundParser 4from breathe.project import ProjectInfo 5from breathe.renderer import RenderContext 6from breathe.renderer.filter import Filter 7from breathe.renderer.target import TargetHandler 8 9from sphinx import addnodes 10from sphinx.application import Sphinx 11from sphinx.directives import ObjectDescription 12from sphinx.domains import cpp, c, python 13from sphinx.util.nodes import nested_parse_with_titles 14 15from docutils import nodes 16from docutils.nodes import Element, Node, TextElement # noqa 17from docutils.statemachine import StringList, UnexpectedIndentationError 18from docutils.parsers.rst.states import Text 19 20try: 21 from sphinxcontrib import phpdomain as php # type: ignore 22except ImportError: 23 php = None 24 25try: 26 from sphinx_csharp import csharp as cs # type: ignore 27except ImportError: 28 cs = None 29 30import re 31import textwrap 32from typing import Callable, cast, Dict, List, Optional, Tuple, Type, Union # noqa 33 34ContentCallback = Callable[[addnodes.desc_content], None] 35Declarator = Union[addnodes.desc_signature, addnodes.desc_signature_line] 36DeclaratorCallback = Callable[[Declarator], None] 37 38_debug_indent = 0 39 40 41class WithContext: 42 def __init__(self, parent: "SphinxRenderer", context: RenderContext): 43 self.context = context 44 self.parent = parent 45 self.previous = None 46 47 def __enter__(self): 48 assert self.previous is None 49 self.previous = self.parent.context 50 self.parent.set_context(self.context) 51 return self 52 53 def __exit__(self, et, ev, bt): 54 self.parent.context = self.previous 55 self.previous = None 56 57 58class BaseObject: 59 # Use this class as the first base class to make sure the overrides are used. 60 # Set the content_callback attribute to a function taking a docutils node. 61 62 def transform_content(self, contentnode: addnodes.desc_content) -> None: 63 super().transform_content(contentnode) # type: ignore 64 callback = getattr(self, "breathe_content_callback", None) 65 if callback is None: 66 return 67 callback(contentnode) 68 69 70# ---------------------------------------------------------------------------- 71 72class CPPClassObject(BaseObject, cpp.CPPClassObject): 73 pass 74 75 76class CPPUnionObject(BaseObject, cpp.CPPUnionObject): 77 pass 78 79 80class CPPFunctionObject(BaseObject, cpp.CPPFunctionObject): 81 pass 82 83 84class CPPMemberObject(BaseObject, cpp.CPPMemberObject): 85 pass 86 87 88class CPPTypeObject(BaseObject, cpp.CPPTypeObject): 89 pass 90 91 92class CPPEnumObject(BaseObject, cpp.CPPEnumObject): 93 pass 94 95 96class CPPEnumeratorObject(BaseObject, cpp.CPPEnumeratorObject): 97 pass 98 99 100# ---------------------------------------------------------------------------- 101 102class CStructObject(BaseObject, c.CStructObject): 103 pass 104 105 106class CUnionObject(BaseObject, c.CUnionObject): 107 pass 108 109 110class CFunctionObject(BaseObject, c.CFunctionObject): 111 pass 112 113 114class CMemberObject(BaseObject, c.CMemberObject): 115 pass 116 117 118class CTypeObject(BaseObject, c.CTypeObject): 119 pass 120 121 122class CEnumObject(BaseObject, c.CEnumObject): 123 pass 124 125 126class CEnumeratorObject(BaseObject, c.CEnumeratorObject): 127 pass 128 129 130class CMacroObject(BaseObject, c.CMacroObject): 131 pass 132 133 134# ---------------------------------------------------------------------------- 135 136class PyFunction(BaseObject, python.PyFunction): 137 pass 138 139 140class PyAttribute(BaseObject, python.PyAttribute): 141 pass 142 143 144class PyClasslike(BaseObject, python.PyClasslike): 145 pass 146 147 148# ---------------------------------------------------------------------------- 149 150# Create multi-inheritance classes to merge BaseObject from Breathe with 151# classes from phpdomain. 152# We use capitalization (and the namespace) to differentiate between the two 153 154if php is not None: 155 class PHPNamespaceLevel(BaseObject, php.PhpNamespacelevel): 156 """Description of a PHP item *in* a namespace (not the space itself).""" 157 pass 158 159 class PHPClassLike(BaseObject, php.PhpClasslike): 160 pass 161 162 class PHPClassMember(BaseObject, php.PhpClassmember): 163 pass 164 165 class PHPGlobalLevel(BaseObject, php.PhpGloballevel): 166 pass 167 168# ---------------------------------------------------------------------------- 169 170if cs is not None: 171 class CSharpCurrentNamespace(BaseObject, cs.CSharpCurrentNamespace): 172 pass 173 174 class CSharpNamespacePlain(BaseObject, cs.CSharpNamespacePlain): 175 pass 176 177 class CSharpClass(BaseObject, cs.CSharpClass): 178 pass 179 180 class CSharpStruct(BaseObject, cs.CSharpStruct): 181 pass 182 183 class CSharpInterface(BaseObject, cs.CSharpInterface): 184 pass 185 186 class CSharpInherits(BaseObject, cs.CSharpInherits): 187 pass 188 189 class CSharpMethod(BaseObject, cs.CSharpMethod): 190 pass 191 192 class CSharpVariable(BaseObject, cs.CSharpVariable): 193 pass 194 195 class CSharpProperty(BaseObject, cs.CSharpProperty): 196 pass 197 198 class CSharpEvent(BaseObject, cs.CSharpEvent): 199 pass 200 201 class CSharpEnum(BaseObject, cs.CSharpEnum): 202 pass 203 204 class CSharpEnumValue(BaseObject, cs.CSharpEnumValue): 205 pass 206 207 class CSharpAttribute(BaseObject, cs.CSharpAttribute): 208 pass 209 210 class CSharpIndexer(BaseObject, cs.CSharpIndexer): 211 pass 212 213 class CSharpXRefRole(BaseObject, cs.CSharpXRefRole): 214 pass 215 216 217# ---------------------------------------------------------------------------- 218 219class DomainDirectiveFactory: 220 # A mapping from node kinds to domain directives and their names. 221 cpp_classes = { 222 'variable': (CPPMemberObject, 'var'), 223 'class': (CPPClassObject, 'class'), 224 'struct': (CPPClassObject, 'struct'), 225 'interface': (CPPClassObject, 'class'), 226 'function': (CPPFunctionObject, 'function'), 227 'friend': (CPPFunctionObject, 'function'), 228 'signal': (CPPFunctionObject, 'function'), 229 'slot': (CPPFunctionObject, 'function'), 230 'enum': (CPPEnumObject, 'enum'), 231 'typedef': (CPPTypeObject, 'type'), 232 'using': (CPPTypeObject, 'type'), 233 'union': (CPPUnionObject, 'union'), 234 'namespace': (CPPTypeObject, 'type'), 235 'enumvalue': (CPPEnumeratorObject, 'enumerator'), 236 'define': (CMacroObject, 'macro'), 237 } 238 c_classes = { 239 'variable': (CMemberObject, 'var'), 240 'function': (CFunctionObject, 'function'), 241 'define': (CMacroObject, 'macro'), 242 'struct': (CStructObject, 'struct'), 243 'union': (CUnionObject, 'union'), 244 'enum': (CEnumObject, 'enum'), 245 'enumvalue': (CEnumeratorObject, 'enumerator'), 246 'typedef': (CTypeObject, 'type'), 247 } 248 python_classes = { 249 # TODO: PyFunction is meant for module-level functions 250 # and PyAttribute is meant for class attributes, not module-level variables. 251 # Somehow there should be made a distinction at some point to get the correct 252 # index-text and whatever other things are different. 253 'function': (PyFunction, 'function'), 254 'variable': (PyAttribute, 'attribute'), 255 'class': (PyClasslike, 'class'), 256 'namespace': (PyClasslike, 'class'), 257 } 258 259 if php is not None: 260 php_classes = { 261 'function': (PHPNamespaceLevel, 'function'), 262 'class': (PHPClassLike, 'class'), 263 'attr': (PHPClassMember, 'attr'), 264 'method': (PHPClassMember, 'method'), 265 'global': (PHPGlobalLevel, 'global'), 266 } 267 php_classes_default = php_classes['class'] # Directive when no matching ones were found 268 269 if cs is not None: 270 cs_classes = { 271 # 'doxygen-name': (CSharp class, key in CSharpDomain.object_types) 272 'namespace': (CSharpNamespacePlain, 'namespace'), 273 274 'class': (CSharpClass, 'class'), 275 'struct': (CSharpStruct, 'struct'), 276 'interface': (CSharpInterface, 'interface'), 277 278 'function': (CSharpMethod, 'function'), 279 'method': (CSharpMethod, 'method'), 280 281 'variable': (CSharpVariable, 'var'), 282 'property': (CSharpProperty, 'property'), 283 'event': (CSharpEvent, 'event'), 284 285 'enum': (CSharpEnum, 'enum'), 286 'enumvalue': (CSharpEnumValue, 'enumerator'), 287 'attribute': (CSharpAttribute, 'attr'), 288 289 # Fallback to cpp domain 290 'typedef': (CPPTypeObject, 'type'), 291 } 292 293 @staticmethod 294 def create(domain: str, args) -> ObjectDescription: 295 cls = cast(Type[ObjectDescription], None) 296 name = cast(str, None) 297 # TODO: remove the 'type: ignore's below at some point 298 # perhaps something to do with the mypy version 299 if domain == 'c': 300 cls, name = DomainDirectiveFactory.c_classes[args[0]] # type: ignore 301 elif domain == 'py': 302 cls, name = DomainDirectiveFactory.python_classes[args[0]] # type: ignore 303 elif php is not None and domain == 'php': 304 separators = php.separators 305 arg_0 = args[0] 306 if any([separators['method'] in n for n in args[1]]): 307 if any([separators['attr'] in n for n in args[1]]): 308 arg_0 = 'attr' 309 else: 310 arg_0 = 'method' 311 else: 312 if arg_0 in ['variable']: 313 arg_0 = 'global' 314 315 if arg_0 in DomainDirectiveFactory.php_classes: 316 cls, name = DomainDirectiveFactory.php_classes[arg_0] # type: ignore 317 else: 318 cls, name = DomainDirectiveFactory.php_classes_default # type: ignore 319 320 elif cs is not None and domain == 'cs': 321 cls, name = DomainDirectiveFactory.cs_classes[args[0]] # type: ignore 322 else: 323 domain = 'cpp' 324 cls, name = DomainDirectiveFactory.cpp_classes[args[0]] # type: ignore 325 # Replace the directive name because domain directives don't know how to handle 326 # Breathe's "doxygen" directives. 327 assert ':' not in name 328 args = [domain + ':' + name] + args[1:] 329 return cls(*args) 330 331 332class NodeFinder(nodes.SparseNodeVisitor): 333 """Find the Docutils desc_signature declarator and desc_content nodes.""" 334 335 def __init__(self, document): 336 super().__init__(document) 337 self.declarator = None 338 self.content = None 339 340 def visit_desc_signature(self, node): 341 # Find the last signature node because it contains the actual declarator 342 # rather than "template <...>". In Sphinx 1.4.1 we'll be able to use sphinx_cpp_tagname: 343 # https://github.com/michaeljones/breathe/issues/242 344 self.declarator = node 345 346 def visit_desc_signature_line(self, node): 347 # In sphinx 1.5, there is now a desc_signature_line node within the desc_signature 348 # This should be used instead 349 self.declarator = node 350 351 def visit_desc_content(self, node): 352 self.content = node 353 354 355def intersperse(iterable, delimiter): 356 it = iter(iterable) 357 yield next(it) 358 for x in it: 359 yield delimiter 360 yield x 361 362 363def get_param_decl(param): 364 def to_string(node): 365 """Convert Doxygen node content to a string.""" 366 result = [] 367 if node is not None: 368 for p in node.content_: 369 value = p.value 370 if not isinstance(value, str): 371 value = value.valueOf_ 372 result.append(value) 373 return ' '.join(result) 374 375 param_type = to_string(param.type_) 376 param_name = param.declname if param.declname else param.defname 377 if not param_name: 378 param_decl = param_type 379 else: 380 param_decl, number_of_subs = re.subn(r'(\((?:\w+::)*[*&]+)(\))', 381 r'\g<1>' + param_name + r'\g<2>', 382 param_type) 383 if number_of_subs == 0: 384 param_decl = param_type + ' ' + param_name 385 if param.array: 386 param_decl += param.array 387 if param.defval: 388 param_decl += ' = ' + to_string(param.defval) 389 390 return param_decl 391 392 393def get_definition_without_template_args(data_object): 394 """ 395 Return data_object.definition removing any template arguments from the class name in the member 396 function. Otherwise links to classes defined in the same template are not generated correctly. 397 398 For example in 'Result<T> A< B<C> >::f' we want to remove the '< B<C> >' part. 399 """ 400 definition = data_object.definition 401 if (len(data_object.bitfield) > 0): 402 definition += " : " + data_object.bitfield 403 qual_name = '::' + data_object.name 404 if definition.endswith(qual_name): 405 qual_name_start = len(definition) - len(qual_name) 406 pos = qual_name_start - 1 407 if definition[pos] == '>': 408 bracket_count = 0 409 # Iterate back through the characters of the definition counting matching braces and 410 # then remove all braces and everything between 411 while pos > 0: 412 if definition[pos] == '>': 413 bracket_count += 1 414 elif definition[pos] == '<': 415 bracket_count -= 1 416 if bracket_count == 0: 417 definition = definition[:pos] + definition[qual_name_start:] 418 break 419 pos -= 1 420 return definition 421 422 423class InlineText(Text): 424 """ 425 Add a custom docutils class to allow parsing inline text. This is to be 426 used inside a @verbatim/@endverbatim block but only the first line is 427 consumed and a inline element is generated as the parent, instead of the 428 paragraph used by Text. 429 """ 430 patterns = {'inlinetext': r''} 431 initial_transitions = [('inlinetext',)] 432 433 def indent(self, match, context, next_state): 434 """ 435 Avoid Text's indent from detecting space prefixed text and 436 doing "funny" stuff; always rely on inlinetext for parsing. 437 """ 438 return self.inlinetext(match, context, next_state) 439 440 def eof(self, context): 441 """ 442 Text.eof() inserts a paragraph, so override it to skip adding elements. 443 """ 444 return [] 445 446 def inlinetext(self, match, context, next_state): 447 """ 448 Called by the StateMachine when an inline element is found (which is 449 any text when this class is added as the single transition. 450 """ 451 startline = self.state_machine.abs_line_number() - 1 452 msg = None 453 try: 454 block = self.state_machine.get_text_block() 455 except UnexpectedIndentationError as err: 456 block, src, srcline = err.args 457 msg = self.reporter.error('Unexpected indentation.', 458 source=src, line=srcline) 459 lines = context + list(block) 460 text, _ = self.inline_text(lines[0], startline) 461 self.parent += text 462 self.parent += msg 463 return [], next_state, [] 464 465 466class SphinxRenderer: 467 """ 468 Doxygen node visitor that converts input into Sphinx/RST representation. 469 Each visit method takes a Doxygen node as an argument and returns a list of RST nodes. 470 """ 471 472 def __init__(self, app: Sphinx, 473 project_info: ProjectInfo, 474 node_stack, 475 state, 476 document: nodes.document, 477 target_handler: TargetHandler, 478 compound_parser: DoxygenCompoundParser, 479 filter_: Filter): 480 self.app = app 481 482 self.project_info = project_info 483 self.qualification_stack = node_stack 484 self.nesting_level = 0 485 self.state = state 486 self.document = document 487 self.target_handler = target_handler 488 self.compound_parser = compound_parser 489 self.filter_ = filter_ 490 491 self.context = None # type: Optional[RenderContext] 492 self.output_defname = True 493 # Nesting level for lists. 494 self.nesting_level = 0 495 496 def set_context(self, context: RenderContext) -> None: 497 self.context = context 498 if self.context.domain == '': 499 self.context.domain = self.get_domain() 500 501 # XXX: fix broken links in XML generated by Doxygen when Doxygen's 502 # SEPARATE_MEMBER_PAGES is set to YES; this function should be harmless 503 # when SEPARATE_MEMBER_PAGES is NO! 504 # 505 # The issue was discussed here: https://github.com/doxygen/doxygen/pull/7971 506 # 507 # A Doxygen anchor consists of a 32-byte string version of the results of 508 # passing in the stringified identifier or prototype that is being "hashed". 509 # An "a" character is then prefixed to mark it as an anchor. Depending on how 510 # the identifier is linked, it can also get a "g" prefix to mean it is part 511 # of a Doxygen group. This results in an id having either 33 or 34 bytes 512 # (containing a "g" or not). Some identifiers, eg enumerators, get twice that 513 # length to have both a unique enum + unique enumerator, and sometimes they 514 # get two "g" characters as prefix instead of one. 515 def _fixup_separate_member_pages(self, refid: str) -> str: 516 if refid: 517 parts = refid.rsplit("_", 1) 518 if len(parts) == 2 and parts[1].startswith("1"): 519 anchorid = parts[1][1:] 520 if len(anchorid) in set([33, 34]) and parts[0].endswith(anchorid): 521 return parts[0][:-len(anchorid)] + parts[1] 522 elif len(anchorid) > 34: 523 index = 0 524 if anchorid.startswith('gg'): 525 index = 1 526 _len = 35 527 elif anchorid.startswith('g'): 528 _len = 34 529 else: 530 _len = 33 531 if parts[0].endswith(anchorid[index:_len]): 532 return parts[0][:-(_len - index)] + parts[1] 533 534 return refid 535 536 def get_refid(self, refid: str) -> str: 537 if self.app.config.breathe_separate_member_pages: # type: ignore 538 refid = self._fixup_separate_member_pages(refid) 539 if self.app.config.breathe_use_project_refids: # type: ignore 540 return "%s%s" % (self.project_info.name(), refid) 541 else: 542 return refid 543 544 def get_domain(self) -> str: 545 """Returns the domain for the current node.""" 546 547 def get_filename(node) -> Optional[str]: 548 """Returns the name of a file where the declaration represented by node is located.""" 549 try: 550 return node.location.file 551 except AttributeError: 552 return None 553 554 self.context = cast(RenderContext, self.context) 555 node_stack = self.context.node_stack 556 node = node_stack[0] 557 # An enumvalue node doesn't have location, so use its parent node for detecting 558 # the domain instead. 559 if isinstance(node, str) or node.node_type == "enumvalue": 560 node = node_stack[1] 561 filename = get_filename(node) 562 if not filename and node.node_type == "compound": 563 file_data = self.compound_parser.parse(node.refid) 564 filename = get_filename(file_data.compounddef) 565 return self.project_info.domain_for_file(filename) if filename else '' 566 567 def join_nested_name(self, names: List[str]) -> str: 568 dom = self.get_domain() 569 sep = '::' if not dom or dom == 'cpp' else '.' 570 return sep.join(names) 571 572 def run_directive(self, obj_type: str, declaration: str, contentCallback: ContentCallback, 573 options={}) -> List[Node]: 574 self.context = cast(RenderContext, self.context) 575 args = [obj_type, [declaration]] + self.context.directive_args[2:] 576 directive = DomainDirectiveFactory.create(self.context.domain, args) 577 assert issubclass(type(directive), BaseObject) 578 directive.breathe_content_callback = contentCallback # type: ignore 579 580 # Translate Breathe's no-link option into the standard noindex option. 581 if 'no-link' in self.context.directive_args[2]: 582 directive.options['noindex'] = True 583 for k, v in options.items(): 584 directive.options[k] = v 585 586 assert self.app.env is not None 587 config = self.app.env.config 588 589 if config.breathe_debug_trace_directives: 590 global _debug_indent 591 print("{}Running directive: .. {}:: {}".format( 592 ' ' * _debug_indent, 593 directive.name, declaration)) 594 _debug_indent += 1 595 596 self.nesting_level += 1 597 nodes = directive.run() 598 self.nesting_level -= 1 599 600 # TODO: the directive_args seems to be reused between different run_directives 601 # so for now, reset the options. 602 # Remove this once the args are given in a different manner. 603 for k, v in options.items(): 604 del directive.options[k] 605 606 if config.breathe_debug_trace_directives: 607 _debug_indent -= 1 608 609 # Filter out outer class names if we are rendering a member as a part of a class content. 610 # In some cases of errors with a declaration there are no nodes 611 # (e.g., variable in function), so perhaps skip (see #671). 612 # If there are nodes, there should be at least 2. 613 if len(nodes) != 0: 614 assert len(nodes) >= 2, nodes 615 rst_node = nodes[1] 616 finder = NodeFinder(rst_node.document) 617 rst_node.walk(finder) 618 619 signode = finder.declarator 620 621 if self.context.child: 622 signode.children = [n for n in signode.children if not n.tagname == 'desc_addname'] 623 return nodes 624 625 def handle_declaration(self, node, declaration: str, *, obj_type: str = None, 626 content_callback: ContentCallback = None, 627 display_obj_type: str = None, 628 declarator_callback: DeclaratorCallback = None, 629 options={}) -> List[Node]: 630 if obj_type is None: 631 obj_type = node.kind 632 if content_callback is None: 633 def content(contentnode): 634 contentnode.extend(self.description(node)) 635 636 content_callback = content 637 declaration = declaration.replace('\n', ' ') 638 nodes_ = self.run_directive(obj_type, declaration, content_callback, options) 639 640 assert self.app.env is not None 641 if self.app.env.config.breathe_debug_trace_doxygen_ids: 642 target = self.create_doxygen_target(node) 643 if len(target) == 0: 644 print("{}Doxygen target: (none)".format(' ' * _debug_indent)) 645 else: 646 print("{}Doxygen target: {}".format(' ' * _debug_indent, 647 target[0]['ids'])) 648 649 # <desc><desc_signature> and then one or more <desc_signature_line> 650 # each <desc_signature_line> has a sphinx_line_type which hints what is present in that line 651 # In some cases of errors with a declaration there are no nodes 652 # (e.g., variable in function), so perhaps skip (see #671). 653 if len(nodes_) == 0: 654 return [] 655 assert len(nodes_) >= 2, nodes_ 656 desc = nodes_[1] 657 assert isinstance(desc, addnodes.desc) 658 assert len(desc) >= 1 659 sig = desc[0] 660 assert isinstance(sig, addnodes.desc_signature) 661 # if may or may not be a multiline signature 662 isMultiline = sig.get('is_multiline', False) 663 declarator = None # type: Optional[Declarator] 664 if isMultiline: 665 for line in sig: 666 assert isinstance(line, addnodes.desc_signature_line) 667 if line.sphinx_line_type == 'declarator': 668 declarator = line 669 else: 670 declarator = sig 671 assert declarator is not None 672 if display_obj_type is not None: 673 n = declarator[0] 674 newStyle = True 675 # the new style was introduced in Sphinx v4 676 if sphinx.version_info[0] < 4: 677 newStyle = False 678 # but only for the C and C++ domains 679 if self.get_domain() and self.get_domain() not in ('c', 'cpp'): 680 newStyle = False 681 if newStyle: 682 # TODO: remove the "type: ignore" when Sphinx >= 4 is required 683 assert isinstance(n, addnodes.desc_sig_keyword) # type: ignore 684 declarator[0] = addnodes.desc_sig_keyword( # type: ignore 685 display_obj_type, display_obj_type) 686 else: 687 assert isinstance(n, addnodes.desc_annotation) 688 assert n.astext()[-1] == " " 689 txt = display_obj_type + ' ' 690 declarator[0] = addnodes.desc_annotation(txt, txt) 691 if not self.app.env.config.breathe_debug_trace_doxygen_ids: 692 target = self.create_doxygen_target(node) 693 declarator.insert(0, target) 694 if declarator_callback: 695 declarator_callback(declarator) 696 return nodes_ 697 698 def get_qualification(self) -> List[str]: 699 if self.nesting_level > 0: 700 return [] 701 702 assert self.app.env is not None 703 config = self.app.env.config 704 if config.breathe_debug_trace_qualification: 705 def debug_print_node(n): 706 return "node_type={}".format(n.node_type) 707 708 global _debug_indent 709 print("{}{}".format(_debug_indent * ' ', 710 debug_print_node(self.qualification_stack[0]))) 711 _debug_indent += 1 712 713 names = [] # type: List[str] 714 for node in self.qualification_stack[1:]: 715 if config.breathe_debug_trace_qualification: 716 print("{}{}".format(_debug_indent * ' ', debug_print_node(node))) 717 if node.node_type == 'ref' and len(names) == 0: 718 if config.breathe_debug_trace_qualification: 719 print("{}{}".format(_debug_indent * ' ', 'res=')) 720 return [] 721 if (node.node_type == 'compound' and 722 node.kind not in ['file', 'namespace', 'group']) or \ 723 node.node_type == 'memberdef': 724 # We skip the 'file' entries because the file name doesn't form part of the 725 # qualified name for the identifier. We skip the 'namespace' entries because if we 726 # find an object through the namespace 'compound' entry in the index.xml then we'll 727 # also have the 'compounddef' entry in our node stack and we'll get it from that. We 728 # need the 'compounddef' entry because if we find the object through the 'file' 729 # entry in the index.xml file then we need to get the namespace name from somewhere 730 names.append(node.name) 731 if (node.node_type == 'compounddef' and node.kind == 'namespace'): 732 # Nested namespaces include their parent namespace(s) in compoundname. ie, 733 # compoundname is 'foo::bar' instead of just 'bar' for namespace 'bar' nested in 734 # namespace 'foo'. We need full compoundname because node_stack doesn't necessarily 735 # include parent namespaces and we stop here in case it does. 736 names.extend(reversed(node.compoundname.split('::'))) 737 break 738 739 names.reverse() 740 741 if config.breathe_debug_trace_qualification: 742 print("{}res={}".format(_debug_indent * ' ', names)) 743 _debug_indent -= 1 744 return names 745 746 # =================================================================================== 747 748 def get_fully_qualified_name(self): 749 750 names = [] 751 node_stack = self.context.node_stack 752 node = node_stack[0] 753 754 # If the node is a namespace, use its name because namespaces are skipped in the main loop. 755 if node.node_type == 'compound' and node.kind == 'namespace': 756 names.append(node.name) 757 758 for node in node_stack: 759 if node.node_type == 'ref' and len(names) == 0: 760 return node.valueOf_ 761 if (node.node_type == 'compound' and 762 node.kind not in ['file', 'namespace', 'group']) or \ 763 node.node_type == 'memberdef': 764 # We skip the 'file' entries because the file name doesn't form part of the 765 # qualified name for the identifier. We skip the 'namespace' entries because if we 766 # find an object through the namespace 'compound' entry in the index.xml then we'll 767 # also have the 'compounddef' entry in our node stack and we'll get it from that. We 768 # need the 'compounddef' entry because if we find the object through the 'file' 769 # entry in the index.xml file then we need to get the namespace name from somewhere 770 names.insert(0, node.name) 771 if (node.node_type == 'compounddef' and node.kind == 'namespace'): 772 # Nested namespaces include their parent namespace(s) in compoundname. ie, 773 # compoundname is 'foo::bar' instead of just 'bar' for namespace 'bar' nested in 774 # namespace 'foo'. We need full compoundname because node_stack doesn't necessarily 775 # include parent namespaces and we stop here in case it does. 776 names.insert(0, node.compoundname) 777 break 778 779 return '::'.join(names) 780 781 def create_template_prefix(self, decl) -> str: 782 if not decl.templateparamlist: 783 return "" 784 nodes = self.render(decl.templateparamlist) 785 return 'template<' + ''.join(n.astext() for n in nodes) + '>' # type: ignore 786 787 def run_domain_directive(self, kind, names): 788 domain_directive = DomainDirectiveFactory.create( 789 self.context.domain, [kind, names] + self.context.directive_args[2:]) 790 791 # Translate Breathe's no-link option into the standard noindex option. 792 if 'no-link' in self.context.directive_args[2]: 793 domain_directive.options['noindex'] = True 794 795 config = self.app.env.config 796 if config.breathe_debug_trace_directives: 797 global _debug_indent 798 print("{}Running directive (old): .. {}:: {}".format( 799 ' ' * _debug_indent, 800 domain_directive.name, ''.join(names))) 801 _debug_indent += 1 802 803 nodes = domain_directive.run() 804 805 if config.breathe_debug_trace_directives: 806 _debug_indent -= 1 807 808 # Filter out outer class names if we are rendering a member as a part of a class content. 809 rst_node = nodes[1] 810 finder = NodeFinder(rst_node.document) 811 rst_node.walk(finder) 812 813 signode = finder.declarator 814 815 if len(names) > 0 and self.context.child: 816 signode.children = [n for n in signode.children if not n.tagname == 'desc_addname'] 817 return nodes 818 819 def create_doxygen_target(self, node) -> List[Element]: 820 """Can be overridden to create a target node which uses the doxygen refid information 821 which can be used for creating links between internal doxygen elements. 822 823 The default implementation should suffice most of the time. 824 """ 825 826 refid = self.get_refid(node.id) 827 return self.target_handler.create_target(refid) 828 829 def title(self, node) -> List[Node]: 830 nodes_ = [] 831 832 # Variable type or function return type 833 nodes_.extend(self.render_optional(node.type_)) 834 if nodes_: 835 nodes_.append(nodes.Text(" ")) 836 nodes_.append(addnodes.desc_name(text=node.name)) 837 return nodes_ 838 839 def description(self, node) -> List[Node]: 840 brief = self.render_optional(node.briefdescription) 841 detailedCand = self.render_optional(node.detaileddescription) 842 # all field_lists must be at the top-level of the desc_content, so pull them up 843 fieldLists = [] # type: List[nodes.field_list] 844 admonitions = [] # type: List[Union[nodes.warning, nodes.note]] 845 846 def pullup(node, typ, dest): 847 for n in node.traverse(typ): 848 del n.parent[n.parent.index(n)] 849 dest.append(n) 850 851 detailed = [] 852 for candNode in detailedCand: 853 pullup(candNode, nodes.field_list, fieldLists) 854 pullup(candNode, nodes.note, admonitions) 855 pullup(candNode, nodes.warning, admonitions) 856 # and collapse paragraphs 857 for para in candNode.traverse(nodes.paragraph): 858 if para.parent and len(para.parent) == 1 \ 859 and isinstance(para.parent, nodes.paragraph): 860 para.replace_self(para.children) 861 862 # and remove empty top-level paragraphs 863 if isinstance(candNode, nodes.paragraph) and len(candNode) == 0: 864 continue 865 detailed.append(candNode) 866 867 # make one big field list instead to the Sphinx transformer can make it pretty 868 if len(fieldLists) > 1: 869 fieldList = nodes.field_list() 870 for fl in fieldLists: 871 fieldList.extend(fl) 872 fieldLists = [fieldList] 873 874 # collapse retvals into a single return field 875 if len(fieldLists) != 0: 876 others: nodes.field = [] 877 retvals: nodes.field = [] 878 for f in fieldLists[0]: 879 fn, fb = f 880 assert len(fn) == 1 881 if fn.astext().startswith("returns "): 882 retvals.append(f) 883 else: 884 others.append(f) 885 if len(retvals) != 0: 886 items: List[nodes.paragraph] = [] 887 for fn, fb in retvals: 888 # we created the retvals before, so we made this prefix 889 assert fn.astext().startswith("returns ") 890 val = nodes.strong('', fn.astext()[8:]) 891 # assumption from visit_docparamlist: fb is a single paragraph or nothing 892 assert len(fb) <= 1, fb 893 bodyNodes = [val, nodes.Text(' -- ')] 894 if len(fb) == 1: 895 assert isinstance(fb[0], nodes.paragraph) 896 bodyNodes.extend(fb[0]) 897 items.append(nodes.paragraph('', '', *bodyNodes)) 898 # only make a bullet list if there are multiple retvals 899 if len(items) == 1: 900 body = items[0] 901 else: 902 body = nodes.bullet_list() 903 for i in items: 904 body.append(nodes.list_item('', i)) 905 fRetvals = nodes.field('', 906 nodes.field_name('', 'returns'), 907 nodes.field_body('', body)) 908 fl = nodes.field_list('', *others, fRetvals) 909 fieldLists = [fl] 910 911 if self.app.config.breathe_order_parameters_first: # type: ignore 912 return brief + detailed + fieldLists + admonitions 913 else: 914 return brief + detailed + admonitions + fieldLists 915 916 def update_signature(self, signature, obj_type): 917 """Update the signature node if necessary, e.g. add qualifiers.""" 918 prefix = obj_type + ' ' 919 annotation = addnodes.desc_annotation(prefix, prefix) 920 if signature[0].tagname != 'desc_name': 921 signature[0] = annotation 922 else: 923 signature.insert(0, annotation) 924 925 def render_declaration(self, node, declaration=None, description=None, **kwargs): 926 if declaration is None: 927 declaration = self.get_fully_qualified_name() 928 obj_type = kwargs.get('objtype', None) 929 if obj_type is None: 930 obj_type = node.kind 931 nodes = self.run_domain_directive(obj_type, [declaration.replace('\n', ' ')]) 932 if self.app.env.config.breathe_debug_trace_doxygen_ids: 933 target = self.create_doxygen_target(node) 934 if len(target) == 0: 935 print("{}Doxygen target (old): (none)".format(' ' * _debug_indent)) 936 else: 937 print("{}Doxygen target (old): {}".format(' ' * _debug_indent, target[0]['ids'])) 938 939 rst_node = nodes[1] 940 finder = NodeFinder(rst_node.document) 941 rst_node.walk(finder) 942 943 signode = finder.declarator 944 contentnode = finder.content 945 946 update_signature = kwargs.get('update_signature', None) 947 if update_signature is not None: 948 update_signature(signode, obj_type) 949 if description is None: 950 description = self.description(node) 951 if not self.app.env.config.breathe_debug_trace_doxygen_ids: 952 target = self.create_doxygen_target(node) 953 signode.insert(0, target) 954 contentnode.extend(description) 955 return nodes 956 957 def visit_doxygen(self, node) -> List[Node]: 958 nodelist = [] 959 960 # Process all the compound children 961 for n in node.get_compound(): 962 nodelist.extend(self.render(n)) 963 return nodelist 964 965 def visit_doxygendef(self, node) -> List[Node]: 966 return self.render(node.compounddef) 967 968 def visit_union(self, node) -> List[Node]: 969 # Read in the corresponding xml file and process 970 file_data = self.compound_parser.parse(node.refid) 971 nodeDef = file_data.compounddef 972 973 self.context = cast(RenderContext, self.context) 974 parent_context = self.context.create_child_context(file_data) 975 new_context = parent_context.create_child_context(nodeDef) 976 977 with WithContext(self, new_context): 978 names = self.get_qualification() 979 if self.nesting_level == 0: 980 names.extend(nodeDef.compoundname.split('::')) 981 else: 982 names.append(nodeDef.compoundname.split('::')[-1]) 983 declaration = self.join_nested_name(names) 984 985 def content(contentnode): 986 if nodeDef.includes: 987 for include in nodeDef.includes: 988 contentnode.extend(self.render(include, 989 new_context.create_child_context(include))) 990 rendered_data = self.render(file_data, parent_context) 991 contentnode.extend(rendered_data) 992 993 nodes = self.handle_declaration(nodeDef, declaration, content_callback=content) 994 return nodes 995 996 def visit_class(self, node) -> List[Node]: 997 # Read in the corresponding xml file and process 998 file_data = self.compound_parser.parse(node.refid) 999 nodeDef = file_data.compounddef 1000 1001 self.context = cast(RenderContext, self.context) 1002 parent_context = self.context.create_child_context(file_data) 1003 new_context = parent_context.create_child_context(nodeDef) 1004 1005 with WithContext(self, new_context): 1006 # Pretend that the signature is being rendered in context of the 1007 # definition, for proper domain detection 1008 kind = nodeDef.kind 1009 # Defer to domains specific directive. 1010 1011 names = self.get_qualification() 1012 # TODO: this breaks if it's a template specialization 1013 # and one of the arguments contain '::' 1014 if self.nesting_level == 0: 1015 names.extend(nodeDef.compoundname.split('::')) 1016 else: 1017 names.append(nodeDef.compoundname.split('::')[-1]) 1018 decls = [ 1019 self.create_template_prefix(nodeDef), 1020 self.join_nested_name(names), 1021 ] 1022 # add base classes 1023 if len(nodeDef.basecompoundref) != 0: 1024 decls.append(':') 1025 first = True 1026 for base in nodeDef.basecompoundref: 1027 if not first: 1028 decls.append(',') 1029 else: 1030 first = False 1031 if base.prot is not None: 1032 decls.append(base.prot) 1033 if base.virt == 'virtual': 1034 decls.append('virtual') 1035 decls.append(base.content_[0].value) 1036 declaration = ' '.join(decls) 1037 1038 def content(contentnode) -> None: 1039 if nodeDef.includes: 1040 for include in nodeDef.includes: 1041 contentnode.extend(self.render(include, 1042 new_context.create_child_context(include))) 1043 rendered_data = self.render(file_data, parent_context) 1044 contentnode.extend(rendered_data) 1045 1046 assert kind in ('class', 'struct', 'interface') 1047 display_obj_type = 'interface' if kind == 'interface' else None 1048 nodes = self.handle_declaration(nodeDef, declaration, content_callback=content, 1049 display_obj_type=display_obj_type) 1050 if 'members-only' in self.context.directive_args[2]: 1051 assert len(nodes) >= 2 1052 assert isinstance(nodes[1], addnodes.desc) 1053 assert len(nodes[1]) >= 2 1054 assert isinstance(nodes[1][1], addnodes.desc_content) 1055 return nodes[1][1].children 1056 return nodes 1057 1058 def visit_namespace(self, node) -> List[Node]: 1059 # Read in the corresponding xml file and process 1060 file_data = self.compound_parser.parse(node.refid) 1061 nodeDef = file_data.compounddef 1062 1063 self.context = cast(RenderContext, self.context) 1064 parent_context = self.context.create_child_context(file_data) 1065 new_context = parent_context.create_child_context(file_data.compounddef) 1066 1067 with WithContext(self, new_context): 1068 # Pretend that the signature is being rendered in context of the 1069 # definition, for proper domain detection 1070 names = self.get_qualification() 1071 if self.nesting_level == 0: 1072 names.extend(nodeDef.compoundname.split('::')) 1073 else: 1074 names.append(nodeDef.compoundname.split('::')[-1]) 1075 declaration = self.join_nested_name(names) 1076 1077 def content(contentnode): 1078 if nodeDef.includes: 1079 for include in nodeDef.includes: 1080 contentnode.extend(self.render(include, 1081 new_context.create_child_context(include))) 1082 rendered_data = self.render(file_data, parent_context) 1083 contentnode.extend(rendered_data) 1084 1085 display_obj_type = 'namespace' if self.get_domain() != 'py' else 'module' 1086 nodes = self.handle_declaration(nodeDef, declaration, content_callback=content, 1087 display_obj_type=display_obj_type) 1088 return nodes 1089 1090 def visit_compound(self, node, render_empty_node=True, **kwargs) -> List[Node]: 1091 # Read in the corresponding xml file and process 1092 file_data = self.compound_parser.parse(node.refid) 1093 1094 def get_node_info(file_data): 1095 return node.name, node.kind 1096 1097 name, kind = kwargs.get('get_node_info', get_node_info)(file_data) 1098 if kind == 'union': 1099 dom = self.get_domain() 1100 assert not dom or dom in ('c', 'cpp') 1101 return self.visit_union(node) 1102 elif kind in ('struct', 'class', 'interface'): 1103 dom = self.get_domain() 1104 if not dom or dom in ('c', 'cpp', 'py', 'cs'): 1105 return self.visit_class(node) 1106 elif kind == 'namespace': 1107 dom = self.get_domain() 1108 if not dom or dom in ('c', 'cpp', 'py', 'cs'): 1109 return self.visit_namespace(node) 1110 1111 self.context = cast(RenderContext, self.context) 1112 parent_context = self.context.create_child_context(file_data) 1113 new_context = parent_context.create_child_context(file_data.compounddef) 1114 rendered_data = self.render(file_data, parent_context) 1115 1116 if not rendered_data and not render_empty_node: 1117 return [] 1118 1119 def render_signature(file_data, doxygen_target, name, kind): 1120 # Defer to domains specific directive. 1121 1122 templatePrefix = self.create_template_prefix(file_data.compounddef) 1123 arg = "%s %s" % (templatePrefix, self.get_fully_qualified_name()) 1124 1125 # add base classes 1126 if kind in ('class', 'struct'): 1127 bs = [] 1128 for base in file_data.compounddef.basecompoundref: 1129 b = [] 1130 if base.prot is not None: 1131 b.append(base.prot) 1132 if base.virt == 'virtual': 1133 b.append("virtual") 1134 b.append(base.content_[0].value) 1135 bs.append(" ".join(b)) 1136 if len(bs) != 0: 1137 arg += " : " 1138 arg += ", ".join(bs) 1139 1140 self.context.directive_args[1] = [arg] 1141 1142 nodes = self.run_domain_directive(kind, self.context.directive_args[1]) 1143 rst_node = nodes[1] 1144 1145 finder = NodeFinder(rst_node.document) 1146 rst_node.walk(finder) 1147 1148 if kind in ('interface', 'namespace'): 1149 # This is not a real C++ declaration type that Sphinx supports, 1150 # so we hax the replacement of it. 1151 finder.declarator[0] = addnodes.desc_annotation(kind + ' ', kind + ' ') 1152 1153 rst_node.children[0].insert(0, doxygen_target) 1154 return nodes, finder.content 1155 1156 refid = self.get_refid(node.refid) 1157 render_sig = kwargs.get('render_signature', render_signature) 1158 with WithContext(self, new_context): 1159 # Pretend that the signature is being rendered in context of the 1160 # definition, for proper domain detection 1161 nodes, contentnode = render_sig( 1162 file_data, self.target_handler.create_target(refid), 1163 name, kind) 1164 1165 if file_data.compounddef.includes: 1166 for include in file_data.compounddef.includes: 1167 contentnode.extend(self.render(include, new_context.create_child_context(include))) 1168 1169 contentnode.extend(rendered_data) 1170 return nodes 1171 1172 def visit_file(self, node) -> List[Node]: 1173 def render_signature(file_data, doxygen_target, name, kind): 1174 self.context = cast(RenderContext, self.context) 1175 options = self.context.directive_args[2] 1176 1177 if "content-only" in options: 1178 rst_node = nodes.container() 1179 else: 1180 rst_node = addnodes.desc() 1181 1182 # Build targets for linking 1183 targets = [] 1184 targets.extend(doxygen_target) 1185 1186 title_signode = addnodes.desc_signature() 1187 title_signode.extend(targets) 1188 1189 # Set up the title 1190 title_signode.append(nodes.emphasis(text=kind)) 1191 title_signode.append(nodes.Text(" ")) 1192 title_signode.append(addnodes.desc_name(text=name)) 1193 1194 rst_node.append(title_signode) 1195 1196 rst_node.document = self.state.document 1197 rst_node['objtype'] = kind 1198 rst_node['domain'] = self.get_domain() if self.get_domain() else 'cpp' 1199 1200 contentnode = addnodes.desc_content() 1201 rst_node.append(contentnode) 1202 1203 return [rst_node], contentnode 1204 1205 return self.visit_compound(node, render_signature=render_signature) 1206 1207 # We store both the identified and appropriate title text here as we want to define the order 1208 # here and the titles for the SectionDefTypeSubRenderer but we don't want the repetition of 1209 # having two lists in case they fall out of sync 1210 # 1211 # If this list is edited, also change the sections option documentation for 1212 # the doxygen(auto)file directive in documentation/source/file.rst. 1213 sections = [ 1214 ("user-defined", "User Defined"), 1215 ("public-type", "Public Types"), 1216 ("public-func", "Public Functions"), 1217 ("public-attrib", "Public Members"), 1218 ("public-slot", "Public Slots"), 1219 ("signal", "Signals"), 1220 ("dcop-func", "DCOP Function"), 1221 ("property", "Properties"), 1222 ("event", "Events"), 1223 ("public-static-func", "Public Static Functions"), 1224 ("public-static-attrib", "Public Static Attributes"), 1225 ("protected-type", "Protected Types"), 1226 ("protected-func", "Protected Functions"), 1227 ("protected-attrib", "Protected Attributes"), 1228 ("protected-slot", "Protected Slots"), 1229 ("protected-static-func", "Protected Static Functions"), 1230 ("protected-static-attrib", "Protected Static Attributes"), 1231 ("package-type", "Package Types"), 1232 ("package-func", "Package Functions"), 1233 ("package-attrib", "Package Attributes"), 1234 ("package-static-func", "Package Static Functions"), 1235 ("package-static-attrib", "Package Static Attributes"), 1236 ("private-type", "Private Types"), 1237 ("private-func", "Private Functions"), 1238 ("private-attrib", "Private Members"), 1239 ("private-slot", "Private Slots"), 1240 ("private-static-func", "Private Static Functions"), 1241 ("private-static-attrib", "Private Static Attributes"), 1242 ("friend", "Friends"), 1243 ("related", "Related"), 1244 ("define", "Defines"), 1245 ("prototype", "Prototypes"), 1246 ("typedef", "Typedefs"), 1247 ("enum", "Enums"), 1248 ("func", "Functions"), 1249 ("var", "Variables"), 1250 ] 1251 1252 def visit_compounddef(self, node) -> List[Node]: 1253 self.context = cast(RenderContext, self.context) 1254 options = self.context.directive_args[2] 1255 section_order = None 1256 if 'sections' in options: 1257 section_order = {sec: i for i, sec in enumerate(options['sections'].split(' '))} 1258 membergroup_order = None 1259 if 'membergroups' in options: 1260 membergroup_order = {sec: i for i, sec in enumerate(options['membergroups'].split(' '))} 1261 nodemap = {} # type: Dict[int, List[Node]] 1262 1263 def addnode(kind, lam): 1264 if section_order is None: 1265 nodemap[len(nodemap)] = lam() 1266 elif kind in section_order: 1267 nodemap.setdefault(section_order[kind], []).extend(lam()) 1268 1269 if 'members-only' not in options: 1270 addnode('briefdescription', lambda: self.render_optional(node.briefdescription)) 1271 addnode('detaileddescription', lambda: self.render_optional(node.detaileddescription)) 1272 1273 def render_derivedcompoundref(node): 1274 if node is None: 1275 return [] 1276 output = self.render_iterable(node) 1277 if not output: 1278 return [] 1279 return [nodes.paragraph( 1280 '', 1281 '', 1282 nodes.Text('Subclassed by '), 1283 *intersperse(output, nodes.Text(', ')) 1284 )] 1285 1286 addnode('derivedcompoundref', 1287 lambda: render_derivedcompoundref(node.derivedcompoundref)) 1288 1289 section_nodelists = {} # type: Dict[str, List[Node]] 1290 1291 # Get all sub sections 1292 for sectiondef in node.sectiondef: 1293 kind = sectiondef.kind 1294 if section_order is not None and kind not in section_order: 1295 continue 1296 header = sectiondef.header 1297 if membergroup_order is not None and header not in membergroup_order: 1298 continue 1299 child_nodes = self.render(sectiondef) 1300 if not child_nodes: 1301 # Skip empty section 1302 continue 1303 rst_node = nodes.container(classes=['breathe-sectiondef']) 1304 rst_node.document = self.state.document 1305 rst_node['objtype'] = kind 1306 rst_node.extend(child_nodes) 1307 # We store the nodes as a list against the kind in a dictionary as the kind can be 1308 # 'user-edited' and that can repeat so this allows us to collect all the 'user-edited' 1309 # entries together 1310 section_nodelists.setdefault(kind, []).append(rst_node) 1311 1312 # Order the results in an appropriate manner 1313 for kind, _ in self.sections: 1314 addnode(kind, lambda: section_nodelists.get(kind, [])) 1315 1316 # Take care of innerclasses 1317 addnode('innerclass', lambda: self.render_iterable(node.innerclass)) 1318 addnode('innernamespace', lambda: self.render_iterable(node.innernamespace)) 1319 1320 if 'inner' in options: 1321 for node in node.innergroup: 1322 file_data = self.compound_parser.parse(node.refid) 1323 inner = file_data.compounddef 1324 addnode('innergroup', lambda: self.visit_compounddef(inner)) 1325 1326 nodelist = [] 1327 for i, nodes_ in sorted(nodemap.items()): 1328 nodelist += nodes_ 1329 1330 return nodelist 1331 1332 section_titles = dict(sections) 1333 1334 def visit_sectiondef(self, node) -> List[Node]: 1335 self.context = cast(RenderContext, self.context) 1336 options = self.context.directive_args[2] 1337 node_list = [] 1338 node_list.extend(self.render_optional(node.description)) 1339 1340 # Get all the memberdef info 1341 node_list.extend(self.render_iterable(node.memberdef)) 1342 1343 if node_list: 1344 if 'members-only' in options: 1345 return node_list 1346 1347 text = self.section_titles[node.kind] 1348 # Override default name for user-defined sections. Use "Unnamed 1349 # Group" if the user didn't name the section 1350 # This is different to Doxygen which will track the groups and name 1351 # them Group1, Group2, Group3, etc. 1352 if node.kind == "user-defined": 1353 if node.header: 1354 text = node.header 1355 else: 1356 text = "Unnamed Group" 1357 1358 # Use rubric for the title because, unlike the docutils element "section", 1359 # it doesn't interfere with the document structure. 1360 idtext = text.replace(' ', '-').lower() 1361 rubric = nodes.rubric(text=text, 1362 classes=['breathe-sectiondef-title'], 1363 ids=['breathe-section-title-' + idtext]) 1364 res = [rubric] # type: List[Node] 1365 return res + node_list 1366 return [] 1367 1368 def visit_docreftext(self, node) -> List[Node]: 1369 nodelist = self.render_iterable(node.content_) 1370 nodelist.extend(self.render_iterable(node.para)) 1371 1372 refid = self.get_refid(node.refid) 1373 1374 nodelist = [ 1375 addnodes.pending_xref( 1376 "", 1377 reftype="ref", 1378 refdomain="std", 1379 refexplicit=True, 1380 refid=refid, 1381 reftarget=refid, 1382 *nodelist 1383 ) 1384 ] 1385 return nodelist 1386 1387 def visit_docheading(self, node) -> List[Node]: 1388 """Heading renderer. 1389 1390 Renders embedded headlines as emphasized text. Different heading levels 1391 are not supported. 1392 """ 1393 nodelist = self.render_iterable(node.content_) 1394 return [nodes.emphasis("", "", *nodelist)] 1395 1396 def visit_docpara(self, node) -> List[Node]: 1397 """ 1398 <para> tags in the Doxygen output tend to contain either text or a single other tag of 1399 interest. So whilst it looks like we're combined descriptions and program listings and 1400 other things, in the end we generally only deal with one per para tag. Multiple 1401 neighbouring instances of these things tend to each be in a separate neighbouring para tag. 1402 """ 1403 1404 nodelist = [] 1405 1406 if self.context and self.context.directive_args[0] == "doxygenpage": 1407 nodelist.extend(self.render_iterable(node.ordered_children)) 1408 else: 1409 contentNodeCands = self.render_iterable(node.content) 1410 # if there are consecutive nodes.Text we should collapse them 1411 # and rerender them to ensure the right paragraphifaction 1412 contentNodes = [] # type: List[Node] 1413 for n in contentNodeCands: 1414 if len(contentNodes) != 0 and isinstance(contentNodes[-1], nodes.Text): 1415 if isinstance(n, nodes.Text): 1416 prev = contentNodes.pop() 1417 contentNodes.extend(self.render_string(prev.astext() + n.astext())) 1418 continue # we have handled this node 1419 contentNodes.append(n) 1420 nodelist.extend(contentNodes) 1421 nodelist.extend(self.render_iterable(node.images)) 1422 1423 paramList = self.render_iterable(node.parameterlist) 1424 defs = [] 1425 fields = [] 1426 for n in self.render_iterable(node.simplesects): 1427 if isinstance(n, nodes.definition_list_item): 1428 defs.append(n) 1429 elif isinstance(n, nodes.field_list): 1430 fields.append(n) 1431 else: 1432 nodelist.append(n) 1433 1434 # note: all these gets pulled up and reordered in description() 1435 if len(defs) != 0: 1436 deflist = nodes.definition_list('', *defs) 1437 nodelist.append(deflist) 1438 nodelist.extend(paramList) 1439 nodelist.extend(fields) 1440 1441 return [nodes.paragraph("", "", *nodelist)] 1442 1443 def visit_docparblock(self, node) -> List[Node]: 1444 return self.render_iterable(node.para) 1445 1446 def visit_docimage(self, node) -> List[Node]: 1447 """Output docutils image node using name attribute from xml as the uri""" 1448 1449 path_to_image = self.project_info.sphinx_abs_path_to_file(node.name) 1450 options = {"uri": path_to_image} 1451 return [nodes.image("", **options)] 1452 1453 def visit_docurllink(self, node) -> List[Node]: 1454 """Url Link Renderer""" 1455 1456 nodelist = self.render_iterable(node.content_) 1457 return [nodes.reference("", "", refuri=node.url, *nodelist)] 1458 1459 def visit_docmarkup(self, node) -> List[Node]: 1460 nodelist = self.render_iterable(node.content_) 1461 creator = nodes.inline # type: Type[TextElement] 1462 if node.type_ == "emphasis": 1463 creator = nodes.emphasis 1464 elif node.type_ == "computeroutput": 1465 creator = nodes.literal 1466 elif node.type_ == "bold": 1467 creator = nodes.strong 1468 elif node.type_ == "superscript": 1469 creator = nodes.superscript 1470 elif node.type_ == "subscript": 1471 creator = nodes.subscript 1472 elif node.type_ == "center": 1473 print("Warning: does not currently handle 'center' text display") 1474 elif node.type_ == "small": 1475 print("Warning: does not currently handle 'small' text display") 1476 return [creator("", "", *nodelist)] 1477 1478 def visit_docsectN(self, node) -> List[Node]: 1479 ''' 1480 Docutils titles are defined by their level inside the document so 1481 the proper structure is only guaranteed by the Doxygen XML. 1482 1483 Doxygen command mapping to XML element name: 1484 @section == sect1, @subsection == sect2, @subsubsection == sect3 1485 ''' 1486 section = nodes.section() 1487 section['ids'].append(self.get_refid(node.id)) 1488 section += nodes.title(node.title, node.title) 1489 section += self.create_doxygen_target(node) 1490 section += self.render_iterable(node.content_) 1491 return [section] 1492 1493 def visit_docsimplesect(self, node) -> List[Node]: 1494 """Other Type documentation such as Warning, Note, Returns, etc""" 1495 1496 # for those that should go into a field list, just render them as that, 1497 # and it will be pulled up later 1498 1499 nodelist = self.render_iterable(node.para) 1500 1501 if node.kind in ('pre', 'post', 'return'): 1502 return [nodes.field_list('', nodes.field( 1503 '', 1504 nodes.field_name('', nodes.Text(node.kind)), 1505 nodes.field_body('', *nodelist) 1506 ))] 1507 elif node.kind == 'warning': 1508 return [nodes.warning('', *nodelist)] 1509 elif node.kind == 'note': 1510 return [nodes.note('', *nodelist)] 1511 1512 if node.kind == "par": 1513 text = self.render(node.title) 1514 else: 1515 text = [nodes.Text(node.kind.capitalize())] 1516 # TODO: is this working as intended? there is something strange with the types 1517 title = nodes.strong("", *text) # type: ignore 1518 1519 term = nodes.term("", "", title) 1520 definition = nodes.definition("", *nodelist) 1521 1522 return [nodes.definition_list_item("", term, definition)] 1523 1524 def visit_doctitle(self, node) -> List[Node]: 1525 return self.render_iterable(node.content_) 1526 1527 def visit_docformula(self, node) -> List[Node]: 1528 nodelist = [] # type: List[Node] 1529 for item in node.content_: 1530 latex = item.getValue() 1531 docname = self.state.document.settings.env.docname 1532 # Strip out the doxygen markup that slips through 1533 # Either inline 1534 if latex.startswith("$") and latex.endswith("$"): 1535 latex = latex[1:-1] 1536 nodelist.append(nodes.math(text=latex, 1537 label=None, 1538 nowrap=False, 1539 docname=docname, 1540 number=None)) 1541 # Else we're multiline 1542 else: 1543 if latex.startswith("\\[") and latex.endswith("\\]"): 1544 latex = latex[2:-2:] 1545 1546 nodelist.append(nodes.math_block(text=latex, 1547 label=None, 1548 nowrap=False, 1549 docname=docname, 1550 number=None)) 1551 return nodelist 1552 1553 def visit_listing(self, node) -> List[Node]: 1554 nodelist = [] # type: List[Node] 1555 for i, item in enumerate(node.codeline): 1556 # Put new lines between the lines. There must be a more pythonic way of doing this 1557 if i: 1558 nodelist.append(nodes.Text("\n")) 1559 nodelist.extend(self.render(item)) 1560 1561 # Add blank string at the start otherwise for some reason it renders 1562 # the pending_xref tags around the kind in plain text 1563 block = nodes.literal_block( 1564 "", 1565 "", 1566 *nodelist 1567 ) 1568 return [block] 1569 1570 def visit_codeline(self, node) -> List[Node]: 1571 return self.render_iterable(node.highlight) 1572 1573 def visit_highlight(self, node) -> List[Node]: 1574 return self.render_iterable(node.content_) 1575 1576 def _nested_inline_parse_with_titles(self, content, node) -> str: 1577 """ 1578 This code is basically a customized nested_parse_with_titles from 1579 docutils, using the InlineText class on the statemachine. 1580 """ 1581 surrounding_title_styles = self.state.memo.title_styles 1582 surrounding_section_level = self.state.memo.section_level 1583 self.state.memo.title_styles = [] 1584 self.state.memo.section_level = 0 1585 try: 1586 return self.state.nested_parse(content, 0, node, match_titles=1, 1587 state_machine_kwargs={ 1588 'state_classes': (InlineText,), 1589 'initial_state': 'InlineText' 1590 }) 1591 finally: 1592 self.state.memo.title_styles = surrounding_title_styles 1593 self.state.memo.section_level = surrounding_section_level 1594 1595 def visit_verbatim(self, node) -> List[Node]: 1596 if not node.text.strip().startswith("embed:rst"): 1597 # Remove trailing new lines. Purely subjective call from viewing results 1598 text = node.text.rstrip() 1599 1600 # Handle has a preformatted text 1601 return [nodes.literal_block(text, text)] 1602 1603 is_inline = False 1604 1605 # do we need to strip leading asterisks? 1606 # NOTE: We could choose to guess this based on every line starting with '*'. 1607 # However This would have a side-effect for any users who have an rst-block 1608 # consisting of a simple bullet list. 1609 # For now we just look for an extended embed tag 1610 if node.text.strip().startswith("embed:rst:leading-asterisk"): 1611 lines = node.text.splitlines() 1612 # Replace the first * on each line with a blank space 1613 lines = map(lambda text: text.replace("*", " ", 1), lines) 1614 node.text = "\n".join(lines) 1615 1616 # do we need to strip leading ///? 1617 elif node.text.strip().startswith("embed:rst:leading-slashes"): 1618 lines = node.text.splitlines() 1619 # Replace the /// on each line with three blank spaces 1620 lines = map(lambda text: text.replace("///", " ", 1), lines) 1621 node.text = "\n".join(lines) 1622 1623 elif node.text.strip().startswith("embed:rst:inline"): 1624 # Inline all text inside the verbatim 1625 node.text = "".join(node.text.splitlines()) 1626 is_inline = True 1627 1628 if is_inline: 1629 text = node.text.replace("embed:rst:inline", "", 1) 1630 else: 1631 # Remove the first line which is "embed:rst[:leading-asterisk]" 1632 text = "\n".join(node.text.split(u"\n")[1:]) 1633 1634 # Remove starting whitespace 1635 text = textwrap.dedent(text) 1636 1637 # Inspired by autodoc.py in Sphinx 1638 rst = StringList() 1639 for line in text.split("\n"): 1640 rst.append(line, "<breathe>") 1641 1642 # Parent node for the generated node subtree 1643 if is_inline: 1644 rst_node = nodes.inline() 1645 else: 1646 rst_node = nodes.paragraph() 1647 rst_node.document = self.state.document 1648 1649 # Generate node subtree 1650 if is_inline: 1651 self._nested_inline_parse_with_titles(rst, rst_node) 1652 else: 1653 nested_parse_with_titles(self.state, rst, rst_node) 1654 1655 return [rst_node] 1656 1657 def visit_inc(self, node) -> List[Node]: 1658 if node.local == u"yes": 1659 text = '#include "%s"' % node.content_[0].getValue() 1660 else: 1661 text = '#include <%s>' % node.content_[0].getValue() 1662 1663 return [nodes.emphasis(text=text)] 1664 1665 def visit_ref(self, node) -> List[Node]: 1666 def get_node_info(file_data): 1667 name = node.content_[0].getValue() 1668 name = name.rsplit("::", 1)[-1] 1669 return name, file_data.compounddef.kind 1670 1671 return self.visit_compound(node, False, get_node_info=get_node_info) 1672 1673 def visit_doclistitem(self, node) -> List[Node]: 1674 """List item renderer. Render all the children depth-first. 1675 Upon return expand the children node list into a docutils list-item. 1676 """ 1677 nodelist = self.render_iterable(node.para) 1678 return [nodes.list_item("", *nodelist)] 1679 1680 numeral_kind = ['arabic', 'loweralpha', 'lowerroman', 'upperalpha', 'upperroman'] 1681 1682 def render_unordered(self, children) -> List[Node]: 1683 nodelist_list = nodes.bullet_list("", *children) 1684 return [nodelist_list] 1685 1686 def render_enumerated(self, children, nesting_level) -> List[Node]: 1687 nodelist_list = nodes.enumerated_list("", *children) 1688 idx = nesting_level % len(SphinxRenderer.numeral_kind) 1689 nodelist_list['enumtype'] = SphinxRenderer.numeral_kind[idx] 1690 nodelist_list['prefix'] = '' 1691 nodelist_list['suffix'] = '.' 1692 return [nodelist_list] 1693 1694 def visit_doclist(self, node) -> List[Node]: 1695 """List renderer 1696 1697 The specifics of the actual list rendering are handled by the 1698 decorator around the generic render function. 1699 Render all the children depth-first. """ 1700 """ Call the wrapped render function. Update the nesting level for the enumerated lists. """ 1701 if node.node_subtype == "itemized": 1702 val = self.render_iterable(node.listitem) 1703 return self.render_unordered(children=val) 1704 elif node.node_subtype == "ordered": 1705 self.nesting_level += 1 1706 val = self.render_iterable(node.listitem) 1707 self.nesting_level -= 1 1708 return self.render_enumerated(children=val, nesting_level=self.nesting_level) 1709 return [] 1710 1711 def visit_compoundref(self, node) -> List[Node]: 1712 nodelist = self.render_iterable(node.content_) 1713 refid = self.get_refid(node.refid) 1714 if refid is not None: 1715 nodelist = [ 1716 addnodes.pending_xref( 1717 "", 1718 reftype="ref", 1719 refdomain="std", 1720 refexplicit=True, 1721 refid=refid, 1722 reftarget=refid, 1723 *nodelist 1724 ) 1725 ] 1726 return nodelist 1727 1728 def visit_docxrefsect(self, node) -> List[Node]: 1729 assert self.app.env is not None 1730 1731 signode = addnodes.desc_signature() 1732 title = node.xreftitle[0] + ':' 1733 titlenode = nodes.emphasis(text=title) 1734 ref = addnodes.pending_xref( 1735 "", 1736 reftype="ref", 1737 refdomain="std", 1738 refexplicit=True, 1739 reftarget=node.id, 1740 refdoc=self.app.env.docname, 1741 *[titlenode]) 1742 signode += ref 1743 1744 nodelist = self.render(node.xrefdescription) 1745 contentnode = addnodes.desc_content() 1746 contentnode += nodelist 1747 1748 descnode = addnodes.desc() 1749 descnode['objtype'] = 'xrefsect' 1750 descnode['domain'] = self.get_domain() if self.get_domain() else 'cpp' 1751 descnode += signode 1752 descnode += contentnode 1753 1754 return [descnode] 1755 1756 def visit_docvariablelist(self, node) -> List[Node]: 1757 output = [] 1758 for varlistentry, listitem in zip(node.varlistentries, node.listitems): 1759 descnode = addnodes.desc() 1760 descnode['objtype'] = 'varentry' 1761 descnode['domain'] = self.get_domain() if self.get_domain() else 'cpp' 1762 signode = addnodes.desc_signature() 1763 signode += self.render_optional(varlistentry) 1764 descnode += signode 1765 contentnode = addnodes.desc_content() 1766 contentnode += self.render_iterable(listitem.para) 1767 descnode += contentnode 1768 output.append(descnode) 1769 return output 1770 1771 def visit_docvarlistentry(self, node) -> List[Node]: 1772 content = node.term.content_ 1773 return self.render_iterable(content) 1774 1775 def visit_docanchor(self, node) -> List[Node]: 1776 return self.create_doxygen_target(node) 1777 1778 def visit_docentry(self, node) -> List[Node]: 1779 col = nodes.entry() 1780 col += self.render_iterable(node.para) 1781 if node.thead == 'yes': 1782 col['heading'] = True 1783 if node.rowspan: 1784 col['morerows'] = int(node.rowspan) - 1 1785 if node.colspan: 1786 col['morecols'] = int(node.colspan) - 1 1787 return [col] 1788 1789 def visit_docrow(self, node) -> List[Node]: 1790 row = nodes.row() 1791 cols = self.render_iterable(node.entry) 1792 if all(col.get('heading', False) for col in cols): 1793 elem = nodes.thead() 1794 else: 1795 elem = nodes.tbody() 1796 row += cols 1797 elem += row 1798 return [elem] 1799 1800 def visit_doctable(self, node) -> List[Node]: 1801 table = nodes.table() 1802 table['classes'] += ['colwidths-auto'] 1803 tgroup = nodes.tgroup(cols=node.cols) 1804 for _ in range(node.cols): 1805 colspec = nodes.colspec() 1806 colspec.attributes['colwidth'] = 'auto' 1807 tgroup += colspec 1808 table += tgroup 1809 rows = self.render_iterable(node.row) 1810 1811 # this code depends on visit_docrow(), and expects the same elements used to 1812 # "envelop" rows there, namely thead and tbody (eg it will need to be updated 1813 # if Doxygen one day adds support for tfoot) 1814 1815 tags = {row.starttag(): [] for row in rows} # type: Dict[str, List] 1816 for row in rows: 1817 tags[row.starttag()].append(row.next_node()) 1818 1819 def merge_row_types(root, elem, elems): 1820 for node in elems: 1821 elem += node 1822 root += elem 1823 1824 for klass in [nodes.thead, nodes.tbody]: 1825 obj = klass() 1826 if obj.starttag() in tags: 1827 merge_row_types(tgroup, obj, tags[obj.starttag()]) 1828 1829 return [table] 1830 1831 def visit_mixedcontainer(self, node: compoundsuper.MixedContainer) -> List[Node]: 1832 return self.render_optional(node.getValue()) 1833 1834 def visit_description(self, node) -> List[Node]: 1835 return self.render_iterable(node.content_) 1836 1837 def visit_linkedtext(self, node) -> List[Node]: 1838 return self.render_iterable(node.content_) 1839 1840 def visit_function(self, node) -> List[Node]: 1841 dom = self.get_domain() 1842 if not dom or dom in ('c', 'cpp', 'py', 'cs'): 1843 names = self.get_qualification() 1844 names.append(node.get_name()) 1845 name = self.join_nested_name(names) 1846 if dom == 'py': 1847 declaration = name + node.get_argsstring() 1848 else: 1849 elements = [self.create_template_prefix(node)] 1850 if node.static == 'yes': 1851 elements.append('static') 1852 if node.inline == 'yes' and dom != 'cs': 1853 elements.append('inline') 1854 if node.kind == 'friend': 1855 elements.append('friend') 1856 if node.virt in ('virtual', 'pure-virtual'): 1857 elements.append('virtual') 1858 if node.explicit == 'yes': 1859 elements.append('explicit') 1860 # TODO: handle constexpr when parser has been updated 1861 # but Doxygen seems to leave it in the type anyway 1862 typ = ''.join(n.astext() for n in self.render(node.get_type())) 1863 # Doxygen sometimes leaves 'static' in the type, 1864 # e.g., for "constexpr static auto f()" 1865 typ = typ.replace('static ', '') 1866 elements.append(typ) 1867 elements.append(name) 1868 elements.append(node.get_argsstring()) 1869 declaration = ' '.join(elements) 1870 nodes = self.handle_declaration(node, declaration) 1871 return nodes 1872 else: 1873 # Get full function signature for the domain directive. 1874 param_list = [] 1875 for param in node.param: 1876 self.context = cast(RenderContext, self.context) 1877 param = self.context.mask_factory.mask(param) 1878 param_decl = get_param_decl(param) 1879 param_list.append(param_decl) 1880 templatePrefix = self.create_template_prefix(node) 1881 signature = '{0}{1}({2})'.format( 1882 templatePrefix, 1883 get_definition_without_template_args(node), 1884 ', '.join(param_list)) 1885 1886 # Add CV-qualifiers. 1887 if node.const == 'yes': 1888 signature += ' const' 1889 # The doxygen xml output doesn't register 'volatile' as the xml attribute for functions 1890 # until version 1.8.8 so we also check argsstring: 1891 # https://bugzilla.gnome.org/show_bug.cgi?id=733451 1892 if node.volatile == 'yes' or node.argsstring.endswith('volatile'): 1893 signature += ' volatile' 1894 1895 if node.refqual == 'lvalue': 1896 signature += '&' 1897 elif node.refqual == 'rvalue': 1898 signature += '&&' 1899 1900 # Add `= 0` for pure virtual members. 1901 if node.virt == 'pure-virtual': 1902 signature += '= 0' 1903 1904 self.context = cast(RenderContext, self.context) 1905 self.context.directive_args[1] = [signature] 1906 1907 nodes = self.run_domain_directive(node.kind, self.context.directive_args[1]) 1908 1909 assert self.app.env is not None 1910 if self.app.env.config.breathe_debug_trace_doxygen_ids: 1911 target = self.create_doxygen_target(node) 1912 if len(target) == 0: 1913 print("{}Doxygen target (old): (none)".format(' ' * _debug_indent)) 1914 else: 1915 print("{}Doxygen target (old): {}".format(' ' * _debug_indent, 1916 target[0]['ids'])) 1917 1918 rst_node = nodes[1] 1919 finder = NodeFinder(rst_node.document) 1920 rst_node.walk(finder) 1921 1922 # Templates have multiple signature nodes in recent versions of Sphinx. 1923 # Insert Doxygen target into the first signature node. 1924 if not self.app.env.config.breathe_debug_trace_doxygen_ids: 1925 target = self.create_doxygen_target(node) 1926 rst_node.children[0].insert(0, target) # type: ignore 1927 1928 finder.content.extend(self.description(node)) 1929 return nodes 1930 1931 def visit_define(self, node) -> List[Node]: 1932 declaration = node.name 1933 if node.param: 1934 declaration += "(" 1935 for i, parameter in enumerate(node.param): 1936 if i: 1937 declaration += ", " 1938 declaration += parameter.defname 1939 declaration += ")" 1940 1941 # TODO: remove this once Sphinx supports definitions for macros 1942 def add_definition(declarator: Declarator) -> None: 1943 if node.initializer and self.app.config.breathe_show_define_initializer: # type: ignore 1944 declarator.append(nodes.Text(" ")) 1945 declarator.extend(self.render(node.initializer)) 1946 1947 return self.handle_declaration(node, declaration, declarator_callback=add_definition) 1948 1949 def visit_enum(self, node) -> List[Node]: 1950 def content(contentnode): 1951 contentnode.extend(self.description(node)) 1952 values = nodes.emphasis("", nodes.Text("Values:")) 1953 title = nodes.paragraph("", "", values) 1954 contentnode += title 1955 enums = self.render_iterable(node.enumvalue) 1956 contentnode.extend(enums) 1957 1958 # TODO: scopedness, Doxygen doesn't seem to generate the xml for that 1959 # TODO: underlying type, Doxygen doesn't seem to generate the xml for that 1960 names = self.get_qualification() 1961 names.append(node.name) 1962 declaration = self.join_nested_name(names) 1963 return self.handle_declaration(node, declaration, content_callback=content) 1964 1965 def visit_enumvalue(self, node) -> List[Node]: 1966 if self.app.config.breathe_show_enumvalue_initializer: # type: ignore 1967 declaration = node.name + self.make_initializer(node) 1968 else: 1969 declaration = node.name 1970 return self.handle_declaration(node, declaration, obj_type='enumvalue') 1971 1972 def visit_typedef(self, node) -> List[Node]: 1973 type_ = ''.join(n.astext() for n in self.render(node.get_type())) # type: ignore 1974 names = self.get_qualification() 1975 names.append(node.get_name()) 1976 name = self.join_nested_name(names) 1977 if node.definition.startswith('using '): 1978 # TODO: looks like Doxygen does not generate the proper XML 1979 # for the template parameter list 1980 declaration = self.create_template_prefix(node) 1981 declaration += ' ' + name + " = " + type_ 1982 else: 1983 # TODO: Both "using" and "typedef" keywords get into this function, 1984 # and if no @typedef comment was added, the definition should 1985 # contain the full text. If a @typedef was used instead, the 1986 # definition has only the typename, which makes it impossible to 1987 # distinguish between them so fallback to "typedef" behavior here. 1988 declaration = ' '.join([type_, name, node.get_argsstring()]) 1989 return self.handle_declaration(node, declaration) 1990 1991 def make_initializer(self, node) -> str: 1992 initializer = node.initializer 1993 signature = [] # type: List[Node] 1994 if initializer: 1995 render_nodes = self.render(initializer) 1996 # Do not append separators for paragraphs. 1997 if not isinstance(render_nodes[0], nodes.paragraph): 1998 separator = ' ' 1999 if not render_nodes[0].startswith('='): # type: ignore 2000 separator += '= ' 2001 signature.append(nodes.Text(separator)) 2002 signature.extend(render_nodes) 2003 return ''.join(n.astext() for n in signature) # type: ignore 2004 2005 def visit_variable(self, node) -> List[Node]: 2006 names = self.get_qualification() 2007 names.append(node.name) 2008 name = self.join_nested_name(names) 2009 dom = self.get_domain() 2010 options = {} 2011 if dom == 'py': 2012 declaration = name 2013 initializer = self.make_initializer(node).strip().lstrip('=').strip() 2014 if len(initializer) != 0: 2015 options['value'] = initializer 2016 else: 2017 elements = [self.create_template_prefix(node)] 2018 if node.static == 'yes': 2019 elements.append('static') 2020 if node.mutable == 'yes': 2021 elements.append('mutable') 2022 typename = ''.join(n.astext() for n in self.render(node.get_type())) 2023 if dom == 'c' and '::' in typename: 2024 typename = typename.replace('::', '.') 2025 elif dom == 'cs': 2026 typename = typename.replace(' ', '') 2027 elements.append(typename) 2028 elements.append(name) 2029 elements.append(node.get_argsstring()) 2030 if dom == 'cs': 2031 if node.get_gettable() or node.get_settable(): 2032 elements.append('{') 2033 if node.get_gettable(): 2034 elements.append('get;') 2035 if node.get_settable(): 2036 elements.append('set;') 2037 elements.append('}') 2038 elements.append(self.make_initializer(node)) 2039 declaration = ' '.join(elements) 2040 if not dom or dom in ('c', 'cpp', 'py', 'cs'): 2041 return self.handle_declaration(node, declaration, options=options) 2042 else: 2043 return self.render_declaration(node, declaration) 2044 2045 def visit_friendclass(self, node) -> List[Node]: 2046 dom = self.get_domain() 2047 assert not dom or dom == 'cpp' 2048 2049 desc = addnodes.desc() 2050 desc['objtype'] = 'friendclass' 2051 desc['domain'] = self.get_domain() if self.get_domain() else 'cpp' 2052 signode = addnodes.desc_signature() 2053 desc += signode 2054 2055 typ = ''.join(n.astext() for n in self.render(node.get_type())) # type: ignore 2056 # in Doxygen < 1.9 the 'friend' part is there, but afterwards not 2057 # https://github.com/michaeljones/breathe/issues/616 2058 assert typ in ("friend class", "friend struct", "class", "struct") 2059 if not typ.startswith('friend '): 2060 typ = 'friend ' + typ 2061 signode += addnodes.desc_annotation(typ, typ) 2062 signode += nodes.Text(' ') 2063 # expr = cpp.CPPExprRole(asCode=False) 2064 # expr.text = node.name 2065 # TODO: set most of the things that SphinxRole.__call__ sets 2066 # signode.extend(expr.run()) 2067 signode += nodes.Text(node.name) 2068 return [desc] 2069 2070 def visit_templateparam(self, node: compound.paramTypeSub, *, 2071 insertDeclNameByParsing: bool = False) -> List[Node]: 2072 nodelist = [] 2073 2074 # Parameter type 2075 if node.type_: 2076 type_nodes = self.render(node.type_) 2077 # Render keywords as annotations for consistency with the cpp domain. 2078 if len(type_nodes) > 0 and isinstance(type_nodes[0], str): 2079 first_node = type_nodes[0] 2080 for keyword in ['typename', 'class']: 2081 if first_node.startswith(keyword + ' '): 2082 type_nodes[0] = nodes.Text(first_node.replace(keyword, '', 1)) 2083 type_nodes.insert(0, addnodes.desc_annotation(keyword, keyword)) 2084 break 2085 nodelist.extend(type_nodes) 2086 2087 # Parameter name 2088 if node.declname: 2089 dom = self.get_domain() 2090 if not dom: 2091 dom = 'cpp' 2092 appendDeclName = True 2093 if insertDeclNameByParsing: 2094 if dom == 'cpp' and sphinx.version_info >= (4, 1, 0): 2095 parser = cpp.DefinitionParser( 2096 ''.join(n.astext() for n in nodelist), 2097 location=self.state.state_machine.get_source_and_line(), 2098 config=self.app.config) 2099 try: 2100 # we really should use _parse_template_parameter() 2101 # but setting a name there is non-trivial, so we use type 2102 ast = parser._parse_type(named='single', outer='templateParam') 2103 assert ast.name is None 2104 nn = cpp.ASTNestedName( 2105 names=[cpp.ASTNestedNameElement( 2106 cpp.ASTIdentifier(node.declname), None)], 2107 templates=[False], rooted=False) 2108 ast.name = nn 2109 # the actual nodes don't matter, as it is astext()-ed later 2110 nodelist = [nodes.Text(str(ast))] 2111 appendDeclName = False 2112 except cpp.DefinitionError: 2113 # happens with "typename ...Args", so for now, just append 2114 pass 2115 2116 if appendDeclName: 2117 if nodelist: 2118 nodelist.append(nodes.Text(" ")) 2119 nodelist.append(nodes.emphasis(text=node.declname)) 2120 elif self.output_defname and node.defname: 2121 # We only want to output the definition name (from the cpp file) if the declaration name 2122 # (from header file) isn't present 2123 if nodelist: 2124 nodelist.append(nodes.Text(" ")) 2125 nodelist.append(nodes.emphasis(text=node.defname)) 2126 2127 # array information 2128 if node.array: 2129 nodelist.append(nodes.Text(node.array)) 2130 2131 # Default value 2132 if node.defval: 2133 nodelist.append(nodes.Text(" = ")) 2134 nodelist.extend(self.render(node.defval)) 2135 2136 return nodelist 2137 2138 def visit_templateparamlist(self, node: compound.templateparamlistTypeSub) -> List[Node]: 2139 nodelist = [] # type: List[Node] 2140 self.output_defname = False 2141 for i, item in enumerate(node.param): 2142 if i: 2143 nodelist.append(nodes.Text(", ")) 2144 nodelist.extend(self.visit_templateparam(item, insertDeclNameByParsing=True)) 2145 self.output_defname = True 2146 return nodelist 2147 2148 def visit_docparamlist(self, node) -> List[Node]: 2149 """Parameter/Exception/TemplateParameter documentation""" 2150 2151 fieldListName = { 2152 "param": "param", 2153 "exception": "throws", 2154 "templateparam": "tparam", 2155 "retval": "returns", 2156 } 2157 2158 # https://docutils.sourceforge.io/docs/ref/doctree.html#field-list 2159 fieldList = nodes.field_list() 2160 for item in node.parameteritem: 2161 # TODO: does item.parameternamelist really have more than 1 parametername? 2162 assert len(item.parameternamelist) <= 1, item.parameternamelist 2163 nameNodes: List[Node] = [] 2164 parameterDirectionNodes = [] 2165 if len(item.parameternamelist) != 0: 2166 paramNameNodes = item.parameternamelist[0].parametername 2167 if len(paramNameNodes) != 0: 2168 nameNodes = [] 2169 for paramName in paramNameNodes: 2170 content = paramName.content_ 2171 # this is really a list of MixedContainer objects, i.e., a generic object 2172 # we assume there is either 1 or 2 elements, if there is 2 the first is the 2173 # parameter direction 2174 assert len(content) == 1 or len(content) == 2, content 2175 thisName = self.render(content[-1]) 2176 if len(nameNodes) != 0: 2177 if node.kind == 'exception': 2178 msg = "Doxygen \\exception commands with multiple names can not be" 2179 msg += " converted to a single :throws: field in Sphinx." 2180 msg += " Exception '{}' suppresed from output.".format( 2181 ''.join(n.astext() for n in thisName)) 2182 self.state.document.reporter.warning(msg) 2183 continue 2184 nameNodes.append(nodes.Text(", ")) 2185 nameNodes.extend(thisName) 2186 if len(content) == 2: 2187 # note, each paramName node seems to have the same direction, 2188 # so just use the last one 2189 dir = ''.join(n.astext() for n in self.render(content[0])).strip() 2190 assert dir in ('[in]', '[out]', '[inout]'), ">" + dir + "<" 2191 parameterDirectionNodes = [ 2192 nodes.strong(dir, dir), 2193 nodes.Text(' ', ' ') 2194 ] 2195 # it seems that Sphinx expects the name to be a single node, 2196 # so let's make it that 2197 txt = fieldListName[node.kind] + ' ' 2198 for n in nameNodes: 2199 txt += n.astext() 2200 name = nodes.field_name('', nodes.Text(txt)) 2201 bodyNodes = self.render_optional(item.parameterdescription) 2202 # TODO: is it correct that bodyNodes is either empty or a single paragraph? 2203 assert len(bodyNodes) <= 1, bodyNodes 2204 if len(bodyNodes) == 1: 2205 assert isinstance(bodyNodes[0], nodes.paragraph) 2206 bodyNodes = [nodes.paragraph( 2207 '', '', *(parameterDirectionNodes + bodyNodes[0].children))] 2208 body = nodes.field_body('', *bodyNodes) 2209 field = nodes.field('', name, body) 2210 fieldList += field 2211 return [fieldList] 2212 2213 def visit_unknown(self, node) -> List[Node]: 2214 """Visit a node of unknown type.""" 2215 return [] 2216 2217 def dispatch_compound(self, node) -> List[Node]: 2218 """Dispatch handling of a compound node to a suitable visit method.""" 2219 if node.kind in ["file", "dir", "page", "example", "group"]: 2220 return self.visit_file(node) 2221 return self.visit_compound(node) 2222 2223 def dispatch_memberdef(self, node) -> List[Node]: 2224 """Dispatch handling of a memberdef node to a suitable visit method.""" 2225 if node.kind in ("function", "signal", "slot") or \ 2226 (node.kind == 'friend' and node.argsstring): 2227 return self.visit_function(node) 2228 if node.kind == "enum": 2229 return self.visit_enum(node) 2230 if node.kind == "typedef": 2231 return self.visit_typedef(node) 2232 if node.kind == "variable": 2233 return self.visit_variable(node) 2234 if node.kind == "property": 2235 # Note: visit like variable for now 2236 return self.visit_variable(node) 2237 if node.kind == "event": 2238 # Note: visit like variable for now 2239 return self.visit_variable(node) 2240 if node.kind == "define": 2241 return self.visit_define(node) 2242 if node.kind == "friend": 2243 # note, friend functions should be dispatched further up 2244 return self.visit_friendclass(node) 2245 return self.render_declaration(node, update_signature=self.update_signature) 2246 2247 # A mapping from node types to corresponding dispatch and visit methods. 2248 # Dispatch methods, as the name suggest, dispatch nodes to appropriate visit 2249 # methods based on node attributes such as kind. 2250 methods = { 2251 "doxygen": visit_doxygen, 2252 "doxygendef": visit_doxygendef, 2253 "compound": dispatch_compound, 2254 "compounddef": visit_compounddef, 2255 "sectiondef": visit_sectiondef, 2256 "memberdef": dispatch_memberdef, 2257 "docreftext": visit_docreftext, 2258 "docheading": visit_docheading, 2259 "docpara": visit_docpara, 2260 "docparblock": visit_docparblock, 2261 "docimage": visit_docimage, 2262 "docurllink": visit_docurllink, 2263 "docmarkup": visit_docmarkup, 2264 "docsect1": visit_docsectN, 2265 "docsect2": visit_docsectN, 2266 "docsect3": visit_docsectN, 2267 "docsimplesect": visit_docsimplesect, 2268 "doctitle": visit_doctitle, 2269 "docformula": visit_docformula, 2270 "listing": visit_listing, 2271 "codeline": visit_codeline, 2272 "highlight": visit_highlight, 2273 "verbatim": visit_verbatim, 2274 "inc": visit_inc, 2275 "ref": visit_ref, 2276 "doclist": visit_doclist, 2277 "doclistitem": visit_doclistitem, 2278 "enumvalue": visit_enumvalue, 2279 "linkedtext": visit_linkedtext, 2280 "compoundref": visit_compoundref, 2281 "mixedcontainer": visit_mixedcontainer, 2282 "description": visit_description, 2283 "templateparamlist": visit_templateparamlist, 2284 "docparamlist": visit_docparamlist, 2285 "docxrefsect": visit_docxrefsect, 2286 "docvariablelist": visit_docvariablelist, 2287 "docvarlistentry": visit_docvarlistentry, 2288 "docanchor": visit_docanchor, 2289 "doctable": visit_doctable, 2290 "docrow": visit_docrow, 2291 "docentry": visit_docentry, 2292 } 2293 2294 def render_string(self, node: str) -> List[Union[nodes.Text, nodes.paragraph]]: 2295 # Skip any nodes that are pure whitespace 2296 # Probably need a better way to do this as currently we're only doing 2297 # it skip whitespace between higher-level nodes, but this will also 2298 # skip any pure whitespace entries in actual content nodes 2299 # 2300 # We counter that second issue slightly by allowing through single white spaces 2301 # 2302 stripped = node.strip() 2303 if stripped: 2304 delimiter = None 2305 if "<linebreak>" in stripped: 2306 delimiter = "<linebreak>" 2307 elif "\n" in stripped: 2308 delimiter = "\n" 2309 if delimiter: 2310 # Render lines as paragraphs because RST doesn't have line breaks. 2311 return [nodes.paragraph('', '', nodes.Text(line.strip())) 2312 for line in node.split(delimiter) if line.strip()] 2313 # importantly, don't strip whitespace as visit_docpara uses it to collapse 2314 # consecutive nodes.Text and rerender them with this function. 2315 return [nodes.Text(node)] 2316 if node == " ": 2317 return [nodes.Text(node)] 2318 return [] 2319 2320 def render(self, node: Node, context: Optional[RenderContext] = None) -> List[Node]: 2321 if context is None: 2322 self.context = cast(RenderContext, self.context) 2323 context = self.context.create_child_context(node) 2324 with WithContext(self, context): 2325 result = [] 2326 self.context = cast(RenderContext, self.context) 2327 if not self.filter_.allow(self.context.node_stack): 2328 pass 2329 elif isinstance(node, str): 2330 result = self.render_string(node) 2331 else: 2332 method = SphinxRenderer.methods.get(node.node_type, SphinxRenderer.visit_unknown) 2333 result = method(self, node) # type: ignore 2334 return result 2335 2336 def render_optional(self, node: Node) -> List[Node]: 2337 """Render a node that can be None.""" 2338 return self.render(node) if node else [] 2339 2340 def render_iterable(self, iterable: List[Node]) -> List[Node]: 2341 output = [] 2342 for entry in iterable: 2343 output.extend(self.render(entry)) 2344 return output 2345 2346 2347def setup(app: Sphinx) -> None: 2348 app.add_config_value('breathe_debug_trace_directives', False, '') 2349 app.add_config_value('breathe_debug_trace_doxygen_ids', False, '') 2350 app.add_config_value('breathe_debug_trace_qualification', False, '') 2351