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