9e5e8e8c40
So that external users of the domain only interested in e.g. referencing roles, can skip tweaks made to Breathe's directives. Signed-off-by: Gerard Marull-Paretas <gerard@teslabs.com>
334 lines
11 KiB
Python
334 lines
11 KiB
Python
"""
|
|
Zephyr Extension
|
|
################
|
|
|
|
Copyright (c) 2023 The Linux Foundation
|
|
SPDX-License-Identifier: Apache-2.0
|
|
|
|
Introduction
|
|
============
|
|
|
|
This extension adds a new ``zephyr`` domain for handling the documentation of various entities
|
|
specific to the Zephyr RTOS project (ex. code samples).
|
|
|
|
Directives
|
|
----------
|
|
|
|
- ``zephyr:code-sample::`` - Defines a code sample.
|
|
The directive takes an ID as the main argument, and accepts ``:name:`` (human-readable short name
|
|
of the sample) and ``:relevant-api:`` (a space separated list of Doxygen group(s) for APIs the
|
|
code sample is a good showcase of) as options.
|
|
The content of the directive is used as the description of the code sample.
|
|
|
|
Example:
|
|
|
|
```
|
|
.. zephyr:code-sample:: blinky
|
|
:name: Blinky
|
|
:relevant-api: gpio_interface
|
|
|
|
Blink an LED forever using the GPIO API.
|
|
```
|
|
|
|
Roles
|
|
-----
|
|
|
|
- ``:zephyr:code-sample:`` - References a code sample.
|
|
The role takes the ID of the code sample as the argument. The role renders as a link to the code
|
|
sample, and the link text is the name of the code sample (or a custom text if an explicit name is
|
|
provided).
|
|
|
|
Example:
|
|
|
|
```
|
|
Check out :zephyr:code-sample:`sample-foo` for an example of how to use the foo API. You may
|
|
also be interested in :zephyr:code-sample:`this one <sample-bar>`.
|
|
```
|
|
|
|
"""
|
|
from typing import Any, Dict, Iterator, List, Tuple
|
|
|
|
from breathe.directives.content_block import DoxygenGroupDirective
|
|
from docutils import nodes
|
|
from docutils.nodes import Node
|
|
from docutils.parsers.rst import Directive, directives
|
|
from sphinx import addnodes
|
|
from sphinx.domains import Domain, ObjType
|
|
from sphinx.roles import XRefRole
|
|
from sphinx.transforms import SphinxTransform
|
|
from sphinx.transforms.post_transforms import SphinxPostTransform
|
|
from sphinx.util import logging
|
|
from sphinx.util.nodes import NodeMatcher, make_refnode
|
|
from zephyr.gh_utils import gh_link_get_url
|
|
|
|
import json
|
|
|
|
__version__ = "0.1.0"
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class CodeSampleNode(nodes.Element):
|
|
pass
|
|
|
|
|
|
class RelatedCodeSamplesNode(nodes.Element):
|
|
pass
|
|
|
|
|
|
class ConvertCodeSampleNode(SphinxTransform):
|
|
default_priority = 100
|
|
|
|
def apply(self):
|
|
matcher = NodeMatcher(CodeSampleNode)
|
|
for node in self.document.traverse(matcher):
|
|
self.convert_node(node)
|
|
|
|
def convert_node(self, node):
|
|
"""
|
|
Transforms a `CodeSampleNode` into a `nodes.section` named after the code sample name.
|
|
|
|
Moves all sibling nodes that are after the `CodeSampleNode` in the documement under this new
|
|
section.
|
|
"""
|
|
parent = node.parent
|
|
siblings_to_move = []
|
|
if parent is not None:
|
|
index = parent.index(node)
|
|
siblings_to_move = parent.children[index + 1 :]
|
|
|
|
# Create a new section
|
|
new_section = nodes.section(ids=[node["id"]])
|
|
new_section += nodes.title(text=node["name"])
|
|
|
|
# Move the sibling nodes under the new section
|
|
new_section.extend(siblings_to_move)
|
|
|
|
# Replace the custom node with the new section
|
|
node.replace_self(new_section)
|
|
|
|
# Remove the moved siblings from their original parent
|
|
for sibling in siblings_to_move:
|
|
parent.remove(sibling)
|
|
|
|
# Set sample description as the meta description of the document for improved SEO
|
|
meta_description = nodes.meta()
|
|
meta_description["name"] = "description"
|
|
meta_description["content"] = node.children[0].astext()
|
|
node.document += meta_description
|
|
|
|
# Similarly, add a node with JSON-LD markup (only renders in HTML output) describing
|
|
# the code sample.
|
|
json_ld = nodes.raw(
|
|
"",
|
|
f"""<script type="application/ld+json">
|
|
{json.dumps({
|
|
"@context": "http://schema.org",
|
|
"@type": "SoftwareSourceCode",
|
|
"name": node['name'],
|
|
"description": node.children[0].astext(),
|
|
"codeSampleType": "full",
|
|
"codeRepository": gh_link_get_url(self.app, self.env.docname)
|
|
})}
|
|
</script>""",
|
|
format="html",
|
|
)
|
|
node.document += json_ld
|
|
|
|
|
|
class ProcessRelatedCodeSamplesNode(SphinxPostTransform):
|
|
default_priority = 5 # before ReferencesResolver
|
|
|
|
def run(self, **kwargs: Any) -> None:
|
|
matcher = NodeMatcher(RelatedCodeSamplesNode)
|
|
for node in self.document.traverse(matcher):
|
|
id = node["id"] # the ID of the node is the name of the doxygen group for which we
|
|
# want to list related code samples
|
|
|
|
code_samples = self.env.domaindata["zephyr"]["code-samples"].values()
|
|
# Filter out code samples that don't reference this doxygen group
|
|
code_samples = [
|
|
code_sample for code_sample in code_samples if id in code_sample["relevant-api"]
|
|
]
|
|
|
|
if len(code_samples) > 0:
|
|
admonition = nodes.admonition()
|
|
admonition += nodes.title(text="Related code samples")
|
|
admonition["classes"].append("related-code-samples")
|
|
admonition["classes"].append("dropdown") # used by sphinx-togglebutton extension
|
|
admonition["classes"].append("toggle-shown") # show the content by default
|
|
|
|
sample_dl = nodes.definition_list()
|
|
|
|
for code_sample in sorted(code_samples, key=lambda x: x["name"]):
|
|
term = nodes.term()
|
|
|
|
sample_xref = addnodes.pending_xref(
|
|
"",
|
|
refdomain="zephyr",
|
|
reftype="code-sample",
|
|
reftarget=code_sample["id"],
|
|
refwarn=True,
|
|
)
|
|
sample_xref += nodes.inline(text=code_sample["name"])
|
|
term += sample_xref
|
|
definition = nodes.definition()
|
|
definition += nodes.paragraph(text=code_sample["description"].astext())
|
|
sample_dli = nodes.definition_list_item()
|
|
sample_dli += term
|
|
sample_dli += definition
|
|
sample_dl += sample_dli
|
|
admonition += sample_dl
|
|
|
|
# replace node with the newly created admonition
|
|
node.replace_self(admonition)
|
|
else:
|
|
# remove node if there are no code samples
|
|
node.replace_self([])
|
|
|
|
|
|
class CodeSampleDirective(Directive):
|
|
"""
|
|
A directive for creating a code sample node in the Zephyr documentation.
|
|
"""
|
|
|
|
required_arguments = 1 # ID
|
|
optional_arguments = 0
|
|
option_spec = {"name": directives.unchanged, "relevant-api": directives.unchanged}
|
|
has_content = True
|
|
|
|
def run(self):
|
|
code_sample_id = self.arguments[0]
|
|
env = self.state.document.settings.env
|
|
code_samples = env.domaindata["zephyr"]["code-samples"]
|
|
|
|
if code_sample_id in code_samples:
|
|
logger.warning(
|
|
f"Code sample {code_sample_id} already exists. "
|
|
f"Other instance in {code_samples[code_sample_id]['docname']}",
|
|
location=(env.docname, self.lineno),
|
|
)
|
|
|
|
name = self.options.get("name", code_sample_id)
|
|
relevant_api_list = self.options.get("relevant-api", "").split()
|
|
|
|
# Create a node for description and populate it with parsed content
|
|
description_node = nodes.container(ids=[f"{code_sample_id}-description"])
|
|
self.state.nested_parse(self.content, self.content_offset, description_node)
|
|
|
|
code_sample = {
|
|
"id": code_sample_id,
|
|
"name": name,
|
|
"description": description_node,
|
|
"relevant-api": relevant_api_list,
|
|
"docname": env.docname,
|
|
}
|
|
|
|
domain = env.get_domain("zephyr")
|
|
domain.add_code_sample(code_sample)
|
|
|
|
# Create an instance of the custom node
|
|
code_sample_node = CodeSampleNode()
|
|
code_sample_node["id"] = code_sample_id
|
|
code_sample_node["name"] = name
|
|
code_sample_node += description_node
|
|
|
|
return [code_sample_node]
|
|
|
|
|
|
class ZephyrDomain(Domain):
|
|
"""Zephyr domain"""
|
|
|
|
name = "zephyr"
|
|
label = "Zephyr Project"
|
|
|
|
roles = {
|
|
"code-sample": XRefRole(innernodeclass=nodes.inline, warn_dangling=True),
|
|
}
|
|
|
|
directives = {"code-sample": CodeSampleDirective}
|
|
|
|
object_types: Dict[str, ObjType] = {
|
|
"code-sample": ObjType("code-sample", "code-sample"),
|
|
}
|
|
|
|
initial_data: Dict[str, Any] = {"code-samples": {}}
|
|
|
|
def clear_doc(self, docname: str) -> None:
|
|
self.data["code-samples"] = {
|
|
sample_id: sample_data
|
|
for sample_id, sample_data in self.data["code-samples"].items()
|
|
if sample_data["docname"] != docname
|
|
}
|
|
|
|
def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None:
|
|
self.data["code-samples"].update(otherdata["code-samples"])
|
|
|
|
def get_objects(self):
|
|
for _, code_sample in self.data["code-samples"].items():
|
|
yield (
|
|
code_sample["id"],
|
|
code_sample["name"],
|
|
"code-sample",
|
|
code_sample["docname"],
|
|
code_sample["id"],
|
|
1,
|
|
)
|
|
|
|
# used by Sphinx Immaterial theme
|
|
def get_object_synopses(self) -> Iterator[Tuple[Tuple[str, str], str]]:
|
|
for _, code_sample in self.data["code-samples"].items():
|
|
yield (
|
|
(code_sample["docname"], code_sample["id"]),
|
|
code_sample["description"].astext(),
|
|
)
|
|
|
|
def resolve_xref(self, env, fromdocname, builder, type, target, node, contnode):
|
|
if type == "code-sample":
|
|
code_sample_info = self.data["code-samples"].get(target)
|
|
if code_sample_info:
|
|
if not node.get("refexplicit"):
|
|
contnode = [nodes.Text(code_sample_info["name"])]
|
|
|
|
return make_refnode(
|
|
builder,
|
|
fromdocname,
|
|
code_sample_info["docname"],
|
|
code_sample_info["id"],
|
|
contnode,
|
|
code_sample_info["description"].astext(),
|
|
)
|
|
|
|
def add_code_sample(self, code_sample):
|
|
self.data["code-samples"][code_sample["id"]] = code_sample
|
|
|
|
|
|
class CustomDoxygenGroupDirective(DoxygenGroupDirective):
|
|
"""Monkey patch for Breathe's DoxygenGroupDirective."""
|
|
|
|
def run(self) -> List[Node]:
|
|
nodes = super().run()
|
|
|
|
if self.config.zephyr_breathe_insert_related_samples:
|
|
return [RelatedCodeSamplesNode(id=self.arguments[0]), *nodes]
|
|
else:
|
|
return nodes
|
|
|
|
|
|
def setup(app):
|
|
app.add_config_value("zephyr_breathe_insert_related_samples", False, "env")
|
|
|
|
app.add_domain(ZephyrDomain)
|
|
|
|
app.add_transform(ConvertCodeSampleNode)
|
|
app.add_post_transform(ProcessRelatedCodeSamplesNode)
|
|
|
|
# monkey-patching of Breathe's DoxygenGroupDirective
|
|
app.add_directive("doxygengroup", CustomDoxygenGroupDirective, override=True)
|
|
|
|
return {
|
|
"version": __version__,
|
|
"parallel_read_safe": True,
|
|
"parallel_write_safe": True,
|
|
}
|