1"""
2Copyright (c) 2021 Nordic Semiconductor ASA
3Copyright (c) 2024 The Linux Foundation
4SPDX-License-Identifier: Apache-2.0
5"""
6
7import concurrent.futures
8import os
9from typing import Any
10
11import doxmlparser
12from docutils import nodes
13from doxmlparser.compound import DoxCompoundKind, DoxMemberKind
14from sphinx import addnodes
15from sphinx.application import Sphinx
16from sphinx.domains.c import CXRefRole
17from sphinx.transforms.post_transforms import SphinxPostTransform
18from sphinx.util import logging
19from sphinx.util.docutils import SphinxDirective
20
21logger = logging.getLogger(__name__)
22
23
24KIND_D2S = {
25    DoxMemberKind.DEFINE: "macro",
26    DoxMemberKind.VARIABLE: "var",
27    DoxMemberKind.TYPEDEF: "type",
28    DoxMemberKind.ENUM: "enum",
29    DoxMemberKind.FUNCTION: "func",
30}
31
32
33class DoxygenGroupDirective(SphinxDirective):
34    has_content = False
35    required_arguments = 1
36    optional_arguments = 0
37
38    def run(self):
39
40        desc_node = addnodes.desc()
41        desc_node["domain"] = "c"
42        desc_node["objtype"] = "group"
43
44        title_signode = addnodes.desc_signature()
45        group_xref = addnodes.pending_xref(
46            "",
47            refdomain="c",
48            reftype="group",
49            reftarget=self.arguments[0],
50            refwarn=True,
51        )
52        group_xref += nodes.Text(self.arguments[0])
53        title_signode += group_xref
54
55        desc_node.append(title_signode)
56
57        return [desc_node]
58
59
60class DoxygenReferencer(SphinxPostTransform):
61    """Mapping between Doxygen memberdef kind and Sphinx kinds"""
62
63    default_priority = 5
64
65    def run(self, **kwargs: Any) -> None:
66        for node in self.document.traverse(addnodes.pending_xref):
67            if node.get("refdomain") != "c":
68                continue
69
70            reftype = node.get("reftype")
71
72            # "member", "data" and "var" are equivalent as per Sphinx documentation for C domain
73            if reftype in ("member", "data"):
74                reftype = "var"
75
76            entry = self.app.env.doxybridge_cache.get(reftype)
77            if not entry:
78                continue
79
80            reftarget = node.get("reftarget").replace(".", "::").rstrip("()")
81            id = entry.get(reftarget)
82            if not id:
83                if reftype == "func":
84                    # macros are sometimes referenced as functions, so try that
85                    id = self.app.env.doxybridge_cache.get("macro").get(reftarget)
86                    if not id:
87                        continue
88                else:
89                    continue
90
91            if reftype in ("struct", "union", "group"):
92                doxygen_target = f"{id}.html"
93            else:
94                split = id.split("_")
95                doxygen_target = f"{'_'.join(split[:-1])}.html#{split[-1][1:]}"
96
97            doxygen_target = str(self.app.config.doxybridge_dir) + "/html/" + doxygen_target
98
99            doc_dir = os.path.dirname(self.document.get("source"))
100            doc_dest = os.path.join(
101                self.app.outdir,
102                os.path.relpath(doc_dir, self.app.srcdir),
103            )
104            rel_uri = os.path.relpath(doxygen_target, doc_dest)
105
106            refnode = nodes.reference("", "", internal=True, refuri=rel_uri, reftitle="")
107
108            refnode.append(node[0].deepcopy())
109
110            if reftype == "group":
111                refnode["classes"].append("doxygroup")
112                title = self.app.env.doxybridge_group_titles.get(reftarget, "group")
113                refnode[0] = nodes.Text(title)
114
115            node.replace_self([refnode])
116
117
118def parse_members(sectiondef):
119    cache = {}
120
121    for memberdef in sectiondef.get_memberdef():
122        kind = KIND_D2S.get(memberdef.get_kind())
123        if not kind:
124            continue
125
126        id = memberdef.get_id()
127        if memberdef.get_kind() == DoxMemberKind.VARIABLE:
128            name = memberdef.get_qualifiedname() or memberdef.get_name()
129        else:
130            name = memberdef.get_name()
131
132        cache.setdefault(kind, {})[name] = id
133
134        if memberdef.get_kind() == DoxMemberKind.ENUM:
135            for enumvalue in memberdef.get_enumvalue():
136                enumname = enumvalue.get_name()
137                enumid = enumvalue.get_id()
138                cache.setdefault("enumerator", {})[enumname] = enumid
139
140    return cache
141
142
143def parse_sections(compounddef):
144    cache = {}
145
146    for sectiondef in compounddef.get_sectiondef():
147        members = parse_members(sectiondef)
148        for kind, data in members.items():
149            cache.setdefault(kind, {}).update(data)
150
151    return cache
152
153
154def parse_compound(inDirName, baseName) -> dict:
155    rootObj = doxmlparser.compound.parse(inDirName + "/" + baseName + ".xml", True)
156    cache = {}
157    group_titles = {}
158
159    for compounddef in rootObj.get_compounddef():
160        name = compounddef.get_compoundname()
161        id = compounddef.get_id()
162        kind = None
163        if compounddef.get_kind() == DoxCompoundKind.STRUCT:
164            kind = "struct"
165        elif compounddef.get_kind() == DoxCompoundKind.UNION:
166            kind = "union"
167        elif compounddef.get_kind() == DoxCompoundKind.GROUP:
168            kind = "group"
169            group_titles[name] = compounddef.get_title()
170
171        if kind:
172            cache.setdefault(kind, {})[name] = id
173
174        sections = parse_sections(compounddef)
175        for kind, data in sections.items():
176            cache.setdefault(kind, {}).update(data)
177
178    return cache, group_titles
179
180
181def parse_index(app: Sphinx, inDirName):
182    rootObj = doxmlparser.index.parse(inDirName + "/index.xml", True)
183    compounds = rootObj.get_compound()
184
185    with concurrent.futures.ProcessPoolExecutor() as executor:
186        futures = [
187            executor.submit(parse_compound, inDirName, compound.get_refid())
188            for compound in compounds
189        ]
190        for future in concurrent.futures.as_completed(futures):
191            cache, group_titles = future.result()
192            for kind, data in cache.items():
193                app.env.doxybridge_cache.setdefault(kind, {}).update(data)
194            app.env.doxybridge_group_titles.update(group_titles)
195
196
197def doxygen_parse(app: Sphinx) -> None:
198    if not app.env.doxygen_input_changed:
199        return
200
201    app.env.doxybridge_cache = {
202        "macro": {},
203        "var": {},
204        "type": {},
205        "enum": {},
206        "enumerator": {},
207        "func": {},
208        "union": {},
209        "struct": {},
210        "group": {},
211    }
212
213    app.env.doxybridge_group_titles = {}
214
215    parse_index(app, str(app.config.doxybridge_dir / "xml"))
216
217
218def setup(app: Sphinx) -> dict[str, Any]:
219    app.add_config_value("doxybridge_dir", None, "env")
220
221    app.add_directive("doxygengroup", DoxygenGroupDirective)
222
223    app.add_role_to_domain("c", "group", CXRefRole())
224
225    app.add_post_transform(DoxygenReferencer)
226    app.connect("builder-inited", doxygen_parse)
227
228    return {
229        "version": "0.1",
230        "parallel_read_safe": True,
231        "parallel_write_safe": True,
232    }
233