mirror of
https://github.com/espressif/esp-idf.git
synced 2026-01-09 02:42:37 +00:00
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:
172
tools/cmakev2/esp_docs_cmakev2_extension.py
Normal file
172
tools/cmakev2/esp_docs_cmakev2_extension.py
Normal 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,
|
||||
}
|
||||
Reference in New Issue
Block a user