blob: b15b1fc2f9ac70cec7d44a70d4c09af2bf131c3a [file] [log] [blame]
"""
Copyright (c) 2021 Nordic Semiconductor ASA
Copyright (c) 2024 The Linux Foundation
SPDX-License-Identifier: Apache-2.0
"""
import os
from typing import Any, Dict
import concurrent.futures
from docutils import nodes
from sphinx import addnodes
from sphinx.application import Sphinx
from sphinx.transforms.post_transforms import SphinxPostTransform
from sphinx.util import logging
from sphinx.util.docutils import SphinxDirective
from sphinx.domains.c import CXRefRole
import doxmlparser
from doxmlparser.compound import DoxCompoundKind, DoxMemberKind
logger = logging.getLogger(__name__)
KIND_D2S = {
DoxMemberKind.DEFINE: "macro",
DoxMemberKind.VARIABLE: "var",
DoxMemberKind.TYPEDEF: "type",
DoxMemberKind.ENUM: "enum",
DoxMemberKind.FUNCTION: "func",
}
class DoxygenGroupDirective(SphinxDirective):
has_content = False
required_arguments = 1
optional_arguments = 0
def run(self):
desc_node = addnodes.desc()
desc_node["domain"] = "c"
desc_node["objtype"] = "group"
title_signode = addnodes.desc_signature()
group_xref = addnodes.pending_xref(
"",
refdomain="c",
reftype="group",
reftarget=self.arguments[0],
refwarn=True,
)
group_xref += nodes.Text(self.arguments[0])
title_signode += group_xref
desc_node.append(title_signode)
return [desc_node]
class DoxygenReferencer(SphinxPostTransform):
"""Mapping between Doxygen memberdef kind and Sphinx kinds"""
default_priority = 5
def run(self, **kwargs: Any) -> None:
for node in self.document.traverse(addnodes.pending_xref):
if node.get("refdomain") != "c":
continue
reftype = node.get("reftype")
# "member", "data" and "var" are equivalent as per Sphinx documentation for C domain
if reftype in ("member", "data"):
reftype = "var"
entry = self.app.env.doxybridge_cache.get(reftype)
if not entry:
continue
reftarget = node.get("reftarget").replace(".", "::").rstrip("()")
id = entry.get(reftarget)
if not id:
if reftype == "func":
# macros are sometimes referenced as functions, so try that
id = self.app.env.doxybridge_cache.get("macro").get(reftarget)
if not id:
continue
else:
continue
if reftype in ("struct", "union", "group"):
doxygen_target = f"{id}.html"
else:
split = id.split("_")
doxygen_target = f"{'_'.join(split[:-1])}.html#{split[-1][1:]}"
doxygen_target = str(self.app.config.doxybridge_dir) + "/html/" + doxygen_target
doc_dir = os.path.dirname(self.document.get("source"))
doc_dest = os.path.join(
self.app.outdir,
os.path.relpath(doc_dir, self.app.srcdir),
)
rel_uri = os.path.relpath(doxygen_target, doc_dest)
refnode = nodes.reference("", "", internal=True, refuri=rel_uri, reftitle="")
refnode.append(node[0].deepcopy())
if reftype == "group":
refnode["classes"].append("doxygroup")
title = self.app.env.doxybridge_group_titles.get(reftarget, "group")
refnode[0] = nodes.Text(title)
node.replace_self([refnode])
def parse_members(sectiondef):
cache = {}
for memberdef in sectiondef.get_memberdef():
kind = KIND_D2S.get(memberdef.get_kind())
if not kind:
continue
id = memberdef.get_id()
if memberdef.get_kind() == DoxMemberKind.VARIABLE:
name = memberdef.get_qualifiedname() or memberdef.get_name()
else:
name = memberdef.get_name()
cache.setdefault(kind, {})[name] = id
if memberdef.get_kind() == DoxMemberKind.ENUM:
for enumvalue in memberdef.get_enumvalue():
enumname = enumvalue.get_name()
enumid = enumvalue.get_id()
cache.setdefault("enumerator", {})[enumname] = enumid
return cache
def parse_sections(compounddef):
cache = {}
for sectiondef in compounddef.get_sectiondef():
members = parse_members(sectiondef)
for kind, data in members.items():
cache.setdefault(kind, {}).update(data)
return cache
def parse_compound(inDirName, baseName) -> Dict:
rootObj = doxmlparser.compound.parse(inDirName + "/" + baseName + ".xml", True)
cache = {}
group_titles = {}
for compounddef in rootObj.get_compounddef():
name = compounddef.get_compoundname()
id = compounddef.get_id()
kind = None
if compounddef.get_kind() == DoxCompoundKind.STRUCT:
kind = "struct"
elif compounddef.get_kind() == DoxCompoundKind.UNION:
kind = "union"
elif compounddef.get_kind() == DoxCompoundKind.GROUP:
kind = "group"
group_titles[name] = compounddef.get_title()
if kind:
cache.setdefault(kind, {})[name] = id
sections = parse_sections(compounddef)
for kind, data in sections.items():
cache.setdefault(kind, {}).update(data)
return cache, group_titles
def parse_index(app: Sphinx, inDirName):
rootObj = doxmlparser.index.parse(inDirName + "/index.xml", True)
compounds = rootObj.get_compound()
with concurrent.futures.ProcessPoolExecutor() as executor:
futures = [
executor.submit(parse_compound, inDirName, compound.get_refid())
for compound in compounds
]
for future in concurrent.futures.as_completed(futures):
cache, group_titles = future.result()
for kind, data in cache.items():
app.env.doxybridge_cache.setdefault(kind, {}).update(data)
app.env.doxybridge_group_titles.update(group_titles)
def doxygen_parse(app: Sphinx) -> None:
if not app.env.doxygen_input_changed:
return
app.env.doxybridge_cache = {
"macro": {},
"var": {},
"type": {},
"enum": {},
"enumerator": {},
"func": {},
"union": {},
"struct": {},
"group": {},
}
app.env.doxybridge_group_titles = {}
parse_index(app, str(app.config.doxybridge_dir / "xml"))
def setup(app: Sphinx) -> Dict[str, Any]:
app.add_config_value("doxybridge_dir", None, "env")
app.add_directive("doxygengroup", DoxygenGroupDirective)
app.add_role_to_domain("c", "group", CXRefRole())
app.add_post_transform(DoxygenReferencer)
app.connect("builder-inited", doxygen_parse)
return {
"version": "0.1",
"parallel_read_safe": True,
"parallel_write_safe": True,
}