Skip to content

Commit c8c3a3b

Browse files
gmarullkartben
authored andcommitted
doc: extensions: add doxybridge
Add an initial version of doxybridge, an extension that allows to use Sphinx C domain to automatically reference Doxygen pages. It also introduces minimal support for `.. doxygengroup` directive, which effectively links to the group's Doxygen page. Co-authored-by: Benjamin Cabé <[email protected]> Signed-off-by: Gerard Marull-Paretas <[email protected]> Signed-off-by: Benjamin Cabé <[email protected]>
1 parent 259db8c commit c8c3a3b

File tree

3 files changed

+249
-2
lines changed

3 files changed

+249
-2
lines changed

doc/_extensions/zephyr/domain.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
"""
4949
from typing import Any, Dict, Iterator, List, Tuple
5050

51-
from breathe.directives.content_block import DoxygenGroupDirective
5251
from docutils import nodes
5352
from docutils.nodes import Node
5453
from docutils.parsers.rst import Directive, directives
@@ -59,6 +58,7 @@
5958
from sphinx.transforms.post_transforms import SphinxPostTransform
6059
from sphinx.util import logging
6160
from sphinx.util.nodes import NodeMatcher, make_refnode
61+
from zephyr.doxybridge import DoxygenGroupDirective
6262
from zephyr.gh_utils import gh_link_get_url
6363

6464
import json
@@ -323,7 +323,7 @@ def setup(app):
323323
app.add_transform(ConvertCodeSampleNode)
324324
app.add_post_transform(ProcessRelatedCodeSamplesNode)
325325

326-
# monkey-patching of Breathe's DoxygenGroupDirective
326+
# monkey-patching of the DoxygenGroupDirective
327327
app.add_directive("doxygengroup", CustomDoxygenGroupDirective, override=True)
328328

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

doc/_static/css/custom.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -953,3 +953,15 @@ dark-mode-toggle::part(toggleLabel){
953953
#search-se-menu ul li.selected .fa-check {
954954
display: inline;
955955
}
956+
957+
.doxygroup::after {
958+
content: 'Doxygen';
959+
display: inline-block;
960+
background-color: var(--admonition-note-title-background-color);
961+
color: var(--admonition-note-title-color);
962+
padding: 2px 8px;
963+
border-radius: 12px;
964+
margin-left: 8px;
965+
font-size: 0.875em;
966+
font-weight: bold;
967+
}

0 commit comments

Comments
 (0)