Skip to content

Commit bcf75d5

Browse files
docs: Add posibility to define external struct and files
- Add :external: prefix to define that something should be found in the external link. For example, functions and structs from sdk-nrf or Zephyr. Signed-off-by: Arkadiusz Balys <arkadiusz.balys@nordicsemi.no>
1 parent 76b2414 commit bcf75d5

5 files changed

Lines changed: 913 additions & 1 deletion

File tree

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
# Copyright (c) 2026 Nordic Semiconductor ASA
2+
#
3+
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
4+
5+
"""Sphinx roles for external and local C API / file cross-references."""
6+
7+
from __future__ import annotations
8+
9+
import re
10+
from pathlib import Path
11+
12+
from docutils import nodes
13+
from sphinx.application import Sphinx
14+
from sphinx.environment import BuildEnvironment
15+
from sphinx.util.docutils import SphinxRole
16+
17+
from external_code_registry import (
18+
ExternalCodeRegistry,
19+
SUPPORTED_KINDS,
20+
load_documentation_substitutions,
21+
load_external_code_registry,
22+
parse_local_ref,
23+
)
24+
25+
__version__ = '0.0.3'
26+
27+
# ``:external:c:struct:`bt_uuid``` is rewritten to ``:external-c-struct:`bt_uuid```
28+
# before parsing so nested ``:c:struct:`` markup is not interpreted.
29+
_REWRITE_EXTERNAL_CODE_ROLE_RE = re.compile(
30+
r':externa?l:c:(struct|func|var|enum|macro|type|member):`([^`]+)`'
31+
)
32+
_REWRITE_EXTERNAL_FILE_ROLE_RE = re.compile(r':externa?l:file:`([^`]+)`')
33+
_REWRITE_LOCAL_CODE_ROLE_RE = re.compile(
34+
r':local:c:(struct|func|var|enum|macro|type|member):`([^`]+)`'
35+
)
36+
_REWRITE_LOCAL_FILE_ROLE_RE = re.compile(r':local:file:`([^`]+)`')
37+
38+
39+
class ExternalCodeRefRole(SphinxRole):
40+
kind: str
41+
42+
def run(self) -> tuple[list[nodes.Node], list[nodes.system_message]]:
43+
symbol = self.text.strip()
44+
registry: ExternalCodeRegistry = self.env.external_code_registry
45+
substitutions: dict[str, str] = self.env.external_code_substitutions
46+
url = registry.resolve_url(self.kind, symbol, substitutions)
47+
48+
if url:
49+
ref = nodes.reference(
50+
self.rawtext,
51+
symbol,
52+
refuri=url,
53+
internal=False,
54+
)
55+
return [ref], []
56+
57+
if registry.is_registered(self.kind, symbol):
58+
literal = nodes.literal(self.rawtext, symbol)
59+
return [literal], []
60+
61+
msg = self.state_machine.reporter.warning(
62+
f'external c:{self.kind} reference target not found: {symbol}',
63+
line=self.lineno,
64+
)
65+
literal = nodes.literal(self.rawtext, symbol)
66+
return [literal], [msg]
67+
68+
69+
class ExternalFileRefRole(SphinxRole):
70+
def run(self) -> tuple[list[nodes.Node], list[nodes.system_message]]:
71+
path = self.text.strip()
72+
registry: ExternalCodeRegistry = self.env.external_code_registry
73+
substitutions: dict[str, str] = self.env.external_code_substitutions
74+
url = registry.resolve_file_url(path, substitutions)
75+
76+
if url:
77+
ref = nodes.reference(
78+
self.rawtext,
79+
path,
80+
refuri=url,
81+
internal=False,
82+
)
83+
return [ref], []
84+
85+
if registry.is_file_registered(path):
86+
literal = nodes.literal(self.rawtext, path)
87+
return [literal], []
88+
89+
msg = self.state_machine.reporter.warning(
90+
f'external file reference target not found: {path}',
91+
line=self.lineno,
92+
)
93+
literal = nodes.literal(self.rawtext, path)
94+
return [literal], [msg]
95+
96+
97+
class LocalCodeRefRole(SphinxRole):
98+
kind: str
99+
100+
def run(self) -> tuple[list[nodes.Node], list[nodes.system_message]]:
101+
return _run_local_ref(self)
102+
103+
104+
class LocalFileRefRole(SphinxRole):
105+
def run(self) -> tuple[list[nodes.Node], list[nodes.system_message]]:
106+
return _run_local_ref(self)
107+
108+
109+
def _run_local_ref(role: SphinxRole) -> tuple[list[nodes.Node], list[nodes.system_message]]:
110+
repo_path, display_name = parse_local_ref(role.text)
111+
registry: ExternalCodeRegistry = role.env.external_code_registry
112+
substitutions: dict[str, str] = role.env.external_code_substitutions
113+
repo_root_path = getattr(role.env, 'external_code_repo_root_path', None)
114+
url = registry.resolve_local_repo_url(
115+
repo_path,
116+
substitutions,
117+
repo_root_path=repo_root_path,
118+
)
119+
120+
if url:
121+
ref = nodes.reference(
122+
role.rawtext,
123+
display_name,
124+
refuri=url,
125+
internal=False,
126+
)
127+
return [ref], []
128+
129+
msg = role.state_machine.reporter.warning(
130+
f'local reference could not be resolved: {repo_path}',
131+
line=role.lineno,
132+
)
133+
literal = nodes.literal(role.rawtext, display_name or repo_path)
134+
return [literal], [msg]
135+
136+
137+
def _rewrite_external_roles(_app: Sphinx, _docname: str, source: list[str]) -> None:
138+
for idx, line in enumerate(source):
139+
line = _REWRITE_EXTERNAL_CODE_ROLE_RE.sub(
140+
lambda match: f':external-c-{match.group(1)}:`{match.group(2)}`',
141+
line,
142+
)
143+
line = _REWRITE_EXTERNAL_FILE_ROLE_RE.sub(
144+
lambda match: f':external-file:`{match.group(1)}`',
145+
line,
146+
)
147+
line = _REWRITE_LOCAL_CODE_ROLE_RE.sub(
148+
lambda match: f':local-c-{match.group(1)}:`{match.group(2)}`',
149+
line,
150+
)
151+
source[idx] = _REWRITE_LOCAL_FILE_ROLE_RE.sub(
152+
lambda match: f':local-file:`{match.group(1)}`',
153+
line,
154+
)
155+
156+
157+
def _attach_registry(app: Sphinx, env: BuildEnvironment, _docnames) -> None:
158+
registry_path = Path(app.config.external_code_sources_file)
159+
shortcuts_path = Path(
160+
getattr(app.config, 'external_code_shortcuts_file', app.confdir / 'shortcuts.txt')
161+
)
162+
west_manifest_path = getattr(app.config, 'west_manifest_path', None)
163+
matter_module_path = getattr(app.config, 'matter_module_path', None)
164+
repo_root_path = getattr(app.config, 'repo_root_path', None)
165+
if repo_root_path is None and west_manifest_path is not None:
166+
repo_root_path = str(Path(west_manifest_path).parent)
167+
repo_root = Path(repo_root_path) if repo_root_path else None
168+
substitutions = load_documentation_substitutions(
169+
shortcuts_path,
170+
repo_root_path=repo_root,
171+
west_manifest_path=Path(west_manifest_path) if west_manifest_path else None,
172+
matter_module_path=Path(matter_module_path) if matter_module_path else None,
173+
)
174+
env.external_code_substitutions = substitutions
175+
env.external_code_repo_root_path = repo_root
176+
env.external_code_registry = load_external_code_registry(
177+
registry_path,
178+
substitutions=substitutions,
179+
)
180+
181+
182+
def setup(app: Sphinx) -> dict[str, bool | str]:
183+
app.add_config_value('external_code_sources_file', 'external_code_sources.yaml', 'env')
184+
app.add_config_value('external_code_shortcuts_file', 'shortcuts.txt', 'env')
185+
app.add_config_value('repo_root_path', None, 'env', [str])
186+
187+
for kind in sorted(SUPPORTED_KINDS):
188+
external_role = ExternalCodeRefRole()
189+
external_role.kind = kind
190+
app.add_role(f'external-c-{kind}', external_role)
191+
app.add_role(f'externa-c-{kind}', external_role)
192+
193+
local_role = LocalCodeRefRole()
194+
local_role.kind = kind
195+
app.add_role(f'local-c-{kind}', local_role)
196+
197+
app.add_role('external-file', ExternalFileRefRole())
198+
app.add_role('externa-file', ExternalFileRefRole())
199+
app.add_role('local-file', LocalFileRefRole())
200+
201+
app.connect('source-read', _rewrite_external_roles)
202+
app.connect('env-before-read-docs', _attach_registry)
203+
204+
return {
205+
'version': __version__,
206+
'parallel_read_safe': True,
207+
'parallel_write_safe': True,
208+
}

0 commit comments

Comments
 (0)