1""" 2Zephyr Extension 3################ 4 5Copyright (c) 2023 The Linux Foundation 6SPDX-License-Identifier: Apache-2.0 7 8Introduction 9============ 10 11This extension adds a new ``zephyr`` domain for handling the documentation of various entities 12specific to the Zephyr RTOS project (ex. code samples). 13 14Directives 15---------- 16 17- ``zephyr:code-sample::`` - Defines a code sample. 18 The directive takes an ID as the main argument, and accepts ``:name:`` (human-readable short name 19 of the sample) and ``:relevant-api:`` (a space separated list of Doxygen group(s) for APIs the 20 code sample is a good showcase of) as options. 21 The content of the directive is used as the description of the code sample. 22 23 Example: 24 25 ``` 26 .. zephyr:code-sample:: blinky 27 :name: Blinky 28 :relevant-api: gpio_interface 29 30 Blink an LED forever using the GPIO API. 31 ``` 32 33Roles 34----- 35 36- ``:zephyr:code-sample:`` - References a code sample. 37 The role takes the ID of the code sample as the argument. The role renders as a link to the code 38 sample, and the link text is the name of the code sample (or a custom text if an explicit name is 39 provided). 40 41 Example: 42 43 ``` 44 Check out :zephyr:code-sample:`sample-foo` for an example of how to use the foo API. You may 45 also be interested in :zephyr:code-sample:`this one <sample-bar>`. 46 ``` 47 48""" 49from typing import Any, Dict, Iterator, List, Tuple 50 51from breathe.directives.content_block import DoxygenGroupDirective 52from docutils import nodes 53from docutils.nodes import Node 54from docutils.parsers.rst import Directive, directives 55from sphinx import addnodes 56from sphinx.domains import Domain, ObjType 57from sphinx.roles import XRefRole 58from sphinx.transforms import SphinxTransform 59from sphinx.transforms.post_transforms import SphinxPostTransform 60from sphinx.util import logging 61from sphinx.util.nodes import NodeMatcher, make_refnode 62 63__version__ = "0.1.0" 64 65logger = logging.getLogger(__name__) 66 67 68class CodeSampleNode(nodes.Element): 69 pass 70 71 72class RelatedCodeSamplesNode(nodes.Element): 73 pass 74 75 76class ConvertCodeSampleNode(SphinxTransform): 77 default_priority = 100 78 79 def apply(self): 80 matcher = NodeMatcher(CodeSampleNode) 81 for node in self.document.traverse(matcher): 82 self.convert_node(node) 83 84 def convert_node(self, node): 85 """ 86 Transforms a `CodeSampleNode` into a `nodes.section` named after the code sample name. 87 88 Moves all sibling nodes that are after the `CodeSampleNode` in the documement under this new 89 section. 90 """ 91 parent = node.parent 92 siblings_to_move = [] 93 if parent is not None: 94 index = parent.index(node) 95 siblings_to_move = parent.children[index + 1 :] 96 97 # Create a new section 98 new_section = nodes.section(ids=[node["id"]]) 99 new_section += nodes.title(text=node["name"]) 100 101 # Move existing content from the custom node to the new section 102 new_section.extend(node.children) 103 104 # Move the sibling nodes under the new section 105 new_section.extend(siblings_to_move) 106 107 # Replace the custom node with the new section 108 node.replace_self(new_section) 109 110 # Remove the moved siblings from their original parent 111 for sibling in siblings_to_move: 112 parent.remove(sibling) 113 114 115class ProcessRelatedCodeSamplesNode(SphinxPostTransform): 116 default_priority = 5 # before ReferencesResolver 117 118 def run(self, **kwargs: Any) -> None: 119 matcher = NodeMatcher(RelatedCodeSamplesNode) 120 for node in self.document.traverse(matcher): 121 id = node["id"] # the ID of the node is the name of the doxygen group for which we 122 # want to list related code samples 123 124 code_samples = self.env.domaindata["zephyr"]["code-samples"].values() 125 # Filter out code samples that don't reference this doxygen group 126 code_samples = [ 127 code_sample for code_sample in code_samples if id in code_sample["relevant-api"] 128 ] 129 130 if len(code_samples) > 0: 131 admonition = nodes.admonition() 132 admonition += nodes.title(text="Related code samples") 133 admonition["collapsible"] = "" # used by sphinx-immaterial theme 134 admonition["classes"].append("related-code-samples") 135 admonition["classes"].append("dropdown") # used by sphinx-togglebutton extension 136 sample_ul = nodes.bullet_list() 137 for code_sample in sorted(code_samples, key=lambda x: x["name"]): 138 sample_para = nodes.paragraph() 139 sample_xref = addnodes.pending_xref( 140 "", 141 refdomain="zephyr", 142 reftype="code-sample", 143 reftarget=code_sample["id"], 144 refwarn=True, 145 ) 146 sample_xref += nodes.inline(text=code_sample["name"]) 147 sample_para += sample_xref 148 sample_para += nodes.inline(text=" - ") 149 sample_para += nodes.inline(text=code_sample["description"].astext()) 150 sample_li = nodes.list_item() 151 sample_li += sample_para 152 sample_ul += sample_li 153 admonition += sample_ul 154 155 # replace node with the newly created admonition 156 node.replace_self(admonition) 157 else: 158 # remove node if there are no code samples 159 node.replace_self([]) 160 161 162class CodeSampleDirective(Directive): 163 """ 164 A directive for creating a code sample node in the Zephyr documentation. 165 """ 166 167 required_arguments = 1 # ID 168 optional_arguments = 0 169 option_spec = {"name": directives.unchanged, "relevant-api": directives.unchanged} 170 has_content = True 171 172 def run(self): 173 code_sample_id = self.arguments[0] 174 env = self.state.document.settings.env 175 code_samples = env.domaindata["zephyr"]["code-samples"] 176 177 if code_sample_id in code_samples: 178 logger.warning( 179 f"Code sample {code_sample_id} already exists. " 180 f"Other instance in {code_samples[code_sample_id]['docname']}", 181 location=(env.docname, self.lineno), 182 ) 183 184 name = self.options.get("name", code_sample_id) 185 relevant_api_list = self.options.get("relevant-api", "").split() 186 187 # Create a node for description and populate it with parsed content 188 description_node = nodes.container(ids=[f"{code_sample_id}-description"]) 189 self.state.nested_parse(self.content, self.content_offset, description_node) 190 191 code_sample = { 192 "id": code_sample_id, 193 "name": name, 194 "description": description_node, 195 "relevant-api": relevant_api_list, 196 "docname": env.docname, 197 } 198 199 domain = env.get_domain("zephyr") 200 domain.add_code_sample(code_sample) 201 202 # Create an instance of the custom node 203 code_sample_node = CodeSampleNode() 204 code_sample_node["id"] = code_sample_id 205 code_sample_node["name"] = name 206 207 return [code_sample_node] 208 209 210class ZephyrDomain(Domain): 211 """Zephyr domain""" 212 213 name = "zephyr" 214 label = "Zephyr Project" 215 216 roles = { 217 "code-sample": XRefRole(innernodeclass=nodes.inline, warn_dangling=True), 218 } 219 220 directives = {"code-sample": CodeSampleDirective} 221 222 object_types: Dict[str, ObjType] = { 223 "code-sample": ObjType("code sample", "code-sample"), 224 } 225 226 initial_data: Dict[str, Any] = {"code-samples": {}} 227 228 def clear_doc(self, docname: str) -> None: 229 self.data["code-samples"] = { 230 sample_id: sample_data 231 for sample_id, sample_data in self.data["code-samples"].items() 232 if sample_data["docname"] != docname 233 } 234 235 def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None: 236 self.data["code-samples"].update(otherdata["code-samples"]) 237 238 def get_objects(self): 239 for _, code_sample in self.data["code-samples"].items(): 240 yield ( 241 code_sample["name"], 242 code_sample["name"], 243 "code sample", 244 code_sample["docname"], 245 code_sample["id"], 246 1, 247 ) 248 249 # used by Sphinx Immaterial theme 250 def get_object_synopses(self) -> Iterator[Tuple[Tuple[str, str], str]]: 251 for _, code_sample in self.data["code-samples"].items(): 252 yield ( 253 (code_sample["docname"], code_sample["id"]), 254 code_sample["description"].astext(), 255 ) 256 257 def resolve_xref(self, env, fromdocname, builder, type, target, node, contnode): 258 if type == "code-sample": 259 code_sample_info = self.data["code-samples"].get(target) 260 if code_sample_info: 261 if not node.get("refexplicit"): 262 contnode = [nodes.Text(code_sample_info["name"])] 263 264 return make_refnode( 265 builder, 266 fromdocname, 267 code_sample_info["docname"], 268 code_sample_info["id"], 269 contnode, 270 code_sample_info["description"].astext(), 271 ) 272 273 def add_code_sample(self, code_sample): 274 self.data["code-samples"][code_sample["id"]] = code_sample 275 276 277class CustomDoxygenGroupDirective(DoxygenGroupDirective): 278 """Monkey patch for Breathe's DoxygenGroupDirective.""" 279 280 def run(self) -> List[Node]: 281 nodes = super().run() 282 return [RelatedCodeSamplesNode(id=self.arguments[0]), *nodes] 283 284 285def setup(app): 286 app.add_domain(ZephyrDomain) 287 288 app.add_transform(ConvertCodeSampleNode) 289 app.add_post_transform(ProcessRelatedCodeSamplesNode) 290 291 # monkey-patching of Breathe's DoxygenGroupDirective 292 app.add_directive("doxygengroup", CustomDoxygenGroupDirective, override=True) 293 294 return { 295 "version": __version__, 296 "parallel_read_safe": True, 297 "parallel_write_safe": True, 298 } 299