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