Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] doc: Add Sphinx extension for code samples #62029

Merged
merged 4 commits into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion boards/arm/nrf52_adafruit_feather/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ the board are working properly with Zephyr:
- :ref:`blinky-sample`
- :ref:`button-sample`
- :ref:`fade-led-sample`
- :ref:`blink-led-sample`
- :ref:`pwm-blinky-sample`
- :ref:`96b_carbon_multi_thread_blinky`

You can build and flash the examples to make sure Zephyr is running correctly on
Expand Down
310 changes: 310 additions & 0 deletions doc/_extensions/zephyr/domain.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
"""
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

__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 :]

# TODO remove once all :ref:`sample-xyz` have migrated to :zephyr:code-sample:`xyz`
# as this is the recommended way to reference code samples going forward.
self.env.app.env.domaindata["std"]["labels"][node["id"]] = (
self.env.docname,
node["id"],
node["name"],
)
self.env.app.env.domaindata["std"]["anonlabels"][node["id"]] = (
self.env.docname,
node["id"],
)

# Create a new section
new_section = nodes.section(ids=[node["id"]])
new_section += nodes.title(text=node["name"])

# Move existing content from the custom node to the new section
new_section.extend(node.children)

# 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)


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["collapsible"] = "" # used by sphinx-immaterial theme
admonition["classes"].append("related-code-samples")
admonition["classes"].append("dropdown") # used by sphinx-togglebutton extension
sample_ul = nodes.bullet_list()
for code_sample in sorted(code_samples, key=lambda x: x["name"]):
sample_para = nodes.paragraph()
sample_xref = addnodes.pending_xref(
"",
refdomain="zephyr",
reftype="code-sample",
reftarget=code_sample["id"],
refwarn=True,
)
sample_xref += nodes.inline(text=code_sample["name"])
sample_para += sample_xref
sample_para += nodes.inline(text=" - ")
sample_para += nodes.inline(text=code_sample["description"].astext())
sample_li = nodes.list_item()
sample_li += sample_para
sample_ul += sample_li
admonition += sample_ul

# 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

return [code_sample_node]


class ZephyrDomain(Domain):
"""Zephyr domain"""

name = "zephyr"
label = "Zephyr Project"

roles = {
"code-sample": XRefRole(innernodeclass=nodes.inline),
}

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["name"],
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"],
)

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()
return [RelatedCodeSamplesNode(id=self.arguments[0]), *nodes]


def setup(app):
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,
}
15 changes: 15 additions & 0 deletions doc/_static/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,21 @@ a.internal:visited code.literal {
color: var(--admonition-tip-title-color);
}

/* Admonition tweaks - sphinx_togglebutton */

.rst-content .admonition.toggle {
overflow: visible;
}

.rst-content .admonition.toggle button {
display: inline-flex;
}

.rst-content .admonition.toggle .tb-icon {
height: 1em;
width: 1em;
}

/* Keyboard shortcuts tweaks */
kbd, .kbd,
.rst-content :not(dl.option-list) > :not(dt):not(kbd):not(.kbd) > kbd,
Expand Down
2 changes: 2 additions & 0 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@
"zephyr.vcs_link",
"notfound.extension",
"sphinx_copybutton",
"sphinx_togglebutton",
kartben marked this conversation as resolved.
Show resolved Hide resolved
"zephyr.external_content",
"zephyr.domain",
]

# Only use SVG converter when it is really needed, e.g. LaTeX.
Expand Down
1 change: 1 addition & 0 deletions doc/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ sphinxcontrib-svg2pdfconverter
pygments>=2.9
sphinx-notfound-page
sphinx-copybutton
sphinx-togglebutton

# YAML validation. Used by zephyr_module.
PyYAML>=5.1
Expand Down
10 changes: 6 additions & 4 deletions doc/templates/sample.tmpl
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
.. _descriptive_title_link_name:
.. zephyr:code-sample:: a_unique_id_for_the_sample
:name: A descriptive short name for the sample
:relevant-api: space-separated list of Doxygen groups of APIs this sample is a good showcase of

[A Descriptive Title]
#####################
Short text description of the sample. It is recommended to word this as if you were completing
the sentence "This code sample shows how to ...").

Overview
********
[A short description about the sample and what it does]
[A longer description about the sample and what it does]

Requirements
************
Expand Down
9 changes: 5 additions & 4 deletions samples/basic/blinky/README.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
.. _blinky-sample:
.. zephyr:code-sample:: blinky-sample
:name: Blinky
kartben marked this conversation as resolved.
Show resolved Hide resolved
:relevant-api: gpio_interface

Blinky
######
Blink an LED forever using the GPIO API.

Overview
********
Expand All @@ -15,7 +16,7 @@ The source code shows how to:
#. Configure the GPIO pin as an output
#. Toggle the pin forever

See :ref:`pwm-blinky-sample` for a similar sample that uses the PWM API instead.
See :zephyr:code-sample:`pwm-blinky-sample` for a similar sample that uses the PWM API instead.

.. _blinky-sample-requirements:

Expand Down