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