feat(cmakev2/docs): add esp_docs_cmakev2_extension sphinx extension

Add a Sphinx extension that introduces a new `cmakev2` domain with
multiple directives, allowing for the automatic extraction of
documentation comments from CMake files and their inclusion in the
Sphinx-generated documentation.

Directives:

- `cmakev2:include`: The included CMake file is processed for
  documentation comments within the `#[[api` and `#]]` marks, which
  should contain valid reStructuredText markup.

- `cmakev2:function`: Creates a CMake function node. All function nodes
  are sorted by name and placed into the `_cmakev2_functions` section.

- `cmakev2:macro`: Creates a CMake macro node. All macro nodes are
  sorted by name and placed into the `_cmakev2_macros` section.

- `cmakev2:variable`: Describes a CMake variable node. All variable
  nodes are sorted by name and placed into the `_cmakev2_variables`
  section.

Each node can be referenced with `` :cmakev2:ref:`<node name>` ``, where
the node name is the function, macro, or variable name as used in the
related directive.

Example:

CMake file:
```
 #[[api
        .. cmakev2:function:: idf_flash_binary
 #]]
```

This function can be referenced with `` :cmakev2:ref:`idf_flash_binary` ``
and will be placed in the `.. _cmakev2_functions:` section.

The extension is currently located in esp-idf, but in the future, we
should consider moving it to esp-docs.

Signed-off-by: Frantisek Hrbata <frantisek.hrbata@espressif.com>
This commit is contained in:
Frantisek Hrbata
2025-09-12 15:24:33 +02:00
committed by BOT
parent 0c69d53b68
commit dc846dbe8d

View File

@@ -0,0 +1,172 @@
# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import re
from docutils import nodes # type: ignore
from docutils.statemachine import StringList # type: ignore
from sphinx import addnodes
from sphinx.addnodes import pending_xref
from sphinx.application import Sphinx
from sphinx.builders import Builder
from sphinx.directives import ObjDescT
from sphinx.directives import ObjectDescription
from sphinx.domains import Domain
from sphinx.environment import BuildEnvironment
from sphinx.roles import XRefRole
from sphinx.util.docutils import SphinxDirective
from sphinx.util.nodes import make_refnode
from sphinx.util.nodes import nested_parse_with_titles
RST_COMMENT_RE = re.compile(r'(?sm)^#\[\[api\n(.*?)\n#\]\]')
class CMakeV2IncludeDirective(SphinxDirective):
required_arguments = 1
def run(self) -> list:
env = self.env
rel_filename, filename = env.relfn2path(self.arguments[0])
# Track the file as a dependency so Sphinx rebuilds if it changes.
env.note_dependency(rel_filename)
# Read CMakeLists.txt file content.
with open(filename, encoding='utf-8') as f:
raw_content = f.read()
# Extract all RST comments from the file.
rst_comments = RST_COMMENT_RE.findall(raw_content)
parsed_nodes = []
for rst_comment in rst_comments:
# Temporary node to hold parsed comment.
node = nodes.section()
node.document = self.state.document
string_list = StringList(rst_comment.splitlines(), source='<embedded cmakev2>')
nested_parse_with_titles(self.state, string_list, node)
parsed_nodes.extend(node.children)
if not hasattr(env, 'cmakev2_comment_nodes'):
env.cmakev2_comment_nodes = {}
env.cmakev2_comment_nodes.setdefault(env.docname, []).extend(parsed_nodes)
return []
class CMakeV2Description(ObjectDescription):
def handle_signature(self, sig: str, signode: addnodes.desc_signature) -> ObjDescT:
signode += addnodes.desc_name(text=sig)
return sig
def add_target_and_index(self, name: ObjDescT, sig: str, signode: addnodes.desc_signature) -> None:
# Register object target only (no index)
labelid = f'cmakev2-{self.cmakev2_type}-{sig}'
signode['ids'].append(labelid)
self.env.domaindata['cmakev2']['xrefs'][sig] = (self.env.docname, labelid)
def run(self) -> list[nodes.Node]:
index, node = super().run()
# Add CMakeV2 custom attributes used when function, macro, and variable
# nodes are added to the doctree and sorted.
node['cmakev2-type'] = self.cmakev2_type
node['cmakev2-name'] = self.arguments[0]
return [node]
class CMakeV2VariableDirective(CMakeV2Description):
cmakev2_type = 'variable'
class CMakeV2FunctionDirective(CMakeV2Description):
cmakev2_type = 'function'
class CMakeV2MacroDirective(CMakeV2Description):
cmakev2_type = 'macro'
class CMakeV2Domain(Domain):
name = 'cmakev2'
label = 'ESP-IDF build system v2'
roles = {
'ref': XRefRole(),
}
directives = {
'variable': CMakeV2VariableDirective,
'function': CMakeV2FunctionDirective,
'macro': CMakeV2MacroDirective,
'include': CMakeV2IncludeDirective,
}
initial_data: dict = {
'xrefs': {},
}
def resolve_xref(
self,
env: BuildEnvironment,
fromdocname: str,
builder: Builder,
typ: str,
target: str,
node: pending_xref,
contnode: nodes.Element,
) -> nodes.Element | None:
xref = self.data['xrefs'].get(target)
if xref:
todocname, labelid = xref
# Extract clean text from contnode and wrap in non-literal node
text = contnode.astext()
newnode = nodes.emphasis(text, text)
return make_refnode(builder, fromdocname, todocname, labelid, newnode)
return None
def insert_cmakev2_comment_nodes(app: Sphinx, doctree: nodes.document) -> None:
env = app.builder.env
# Get nodes parsed and created by CMakeV2IncludeDirective.
pending = getattr(env, 'cmakev2_comment_nodes', {}).get(env.docname, [])
# Split nodes parsed in CMakeV2IncludeDirective into buckets based on their
# type.
buckets: dict = {'function': [], 'macro': [], 'variable': []}
for node in pending:
buckets[node['cmakev2-type']].append(node)
# Sort functions, macros, and variables based on their names.
buckets_sorted = {}
for bucket_name, bucket_list in buckets.items():
buckets_sorted[bucket_name] = sorted(bucket_list, key=lambda x: x['cmakev2-name'])
# Traverse through the doctree to locate the target section labels (such as
# _cmakev2_variables) and insert the nodes sorted into the appropriate
# section.
for section in doctree.traverse(nodes.section):
if 'cmakev2-variables' in section['ids']:
section.extend(buckets_sorted['variable'])
elif 'cmakev2-functions' in section['ids']:
section.extend(buckets_sorted['function'])
elif 'cmakev2-macros' in section['ids']:
section.extend(buckets_sorted['macro'])
def setup(app: Sphinx) -> dict:
app.add_domain(CMakeV2Domain)
# The nodes generated with CMakeV2IncludeDirective need to be placed into the
# proper sections (functions, macros, variables), but the section labels
# are not known during the directive run() method. Place the nodes into
# section in the doctree-read event.
app.connect('doctree-read', insert_cmakev2_comment_nodes)
return {
'version': '0.1',
'parallel_read_safe': True,
'parallel_write_safe': True,
}