#!/usr/bin/env python3
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-docstring, invalid-name, unnecessary-pass
"""\
kernel-doc-man
~~~~~~~~~~~~~~
Implementation of the ``kernel-doc-man`` builder.
User documentation see :ref:`man-pages`.
"""
# ==============================================================================
# imports
# ==============================================================================
import collections
import re
from os import path
from docutils import nodes
from docutils.frontend import OptionParser
from docutils.io import FileOutput
from docutils.parsers.rst import Directive
from docutils.transforms import Transform
from docutils.utils import new_document
from sphinx import addnodes
from sphinx.builders.manpage import ManualPageBuilder
from sphinx.util import logging
from sphinx.util.console import bold, darkgreen # pylint: disable=no-name-in-module
from sphinx.util.nodes import inline_all_toctrees
from sphinx.writers.manpage import ManualPageWriter
from .kernel_doc import Container
logger = logging.getLogger(__name__)
# ==============================================================================
# common globals
# ==============================================================================
DEFAULT_MAN_SECT = 9
# The version numbering follows numbering of the specification
# (Documentation/books/kernel-doc-HOWTO).
__version__ = "1.0"
# ==============================================================================
[docs]
def setup(app):
# ==============================================================================
app.add_builder(KernelDocManBuilder)
app.add_directive("kernel-doc-man", KernelDocMan)
app.add_node(
kernel_doc_man,
html=(skip_kernel_doc_man, None),
latex=(skip_kernel_doc_man, None),
texinfo=(skip_kernel_doc_man, None),
text=(skip_kernel_doc_man, None),
man=(skip_kernel_doc_man, None),
)
return dict(version=__version__, parallel_read_safe=True, parallel_write_safe=True)
# ==============================================================================
[docs]
class kernel_doc_man(nodes.Invisible, nodes.Element): # pylint: disable=invalid-name
# ==============================================================================
"""Node to mark a section as *manpage*"""
[docs]
def skip_kernel_doc_man(self, node): # pylint: disable=unused-argument
raise nodes.SkipNode
# ==============================================================================
[docs]
class KernelDocMan(Directive):
# ==============================================================================
required_arguments = 1
optional_arguments = 0
[docs]
def run(self):
man_node = kernel_doc_man()
man_node["manpage"] = self.arguments[0]
return [man_node]
# ==============================================================================
[docs]
class Section2Manpage(Transform):
# ==============================================================================
"""Transforms a *section* tree into an *manpage* tree.
The structural layout of a man-page differs from the one produced, by the
kernel-doc parser. The kernel-doc parser produce reST which fits to *normal*
documentation, e.g. the declaration of a function in reST is like.
.. code-block:: rst
user_function
=============
.. c:function:: int user_function(int a)
The *purpose* description.
:param int a:
Parameter a description
Description
===========
lorem ipsum ..
Return
======
Returns first argument
On the other side, in man-pages it is common (see ``man man-pages``) to
print the *purpose* line in the "NAME" section, function's prototype in the
"SYNOPSIS" section and the parameter description in the "OPTIONS" section::
NAME
user_function -- The *purpose* description.
SYNOPSIS
int user_function(int a)
OPTIONS
a
DESCRIPTION
lorem ipsum
RETURN VALUE
Returns first argument
"""
# The common section order is:
manTitles = [
(re.compile(r"^SYNOPSIS|^DEFINITION", flags=re.I), "SYNOPSIS"),
(re.compile(r"^CONFIG", flags=re.I), "CONFIGURATION"),
(re.compile(r"^DESCR", flags=re.I), "DESCRIPTION"),
(re.compile(r"^OPTION", flags=re.I), "OPTIONS"),
(re.compile(r"^EXIT", flags=re.I), "EXIT STATUS"),
(re.compile(r"^RETURN", flags=re.I), "RETURN VALUE"),
(re.compile(r"^ERROR", flags=re.I), "ERRORS"),
(re.compile(r"^ENVIRON", flags=re.I), "ENVIRONMENT"),
(re.compile(r"^FILE", flags=re.I), "FILES"),
(re.compile(r"^VER", flags=re.I), "VERSIONS"),
(re.compile(r"^ATTR", flags=re.I), "ATTRIBUTES"),
(re.compile(r"^CONFOR", flags=re.I), "CONFORMING TO"),
(re.compile(r"^NOTE", flags=re.I), "NOTES"),
(re.compile(r"^BUG", flags=re.I), "BUGS"),
(re.compile(r"^EXAMPLE", flags=re.I), "EXAMPLE"),
(re.compile(r"^SEE", flags=re.I), "SEE ALSO"),
]
manTitleOrder = [t for r, t in manTitles]
[docs]
@classmethod
def sec2man_get_first_child(cls, subtree, *classes):
for _c in classes:
if subtree is None:
break
idx = subtree.first_child_matching_class(_c)
if idx is None:
subtree = None
break
subtree = subtree[idx]
return subtree
[docs]
def strip_man_info(self):
section = self.document[0]
man_info = Container(authors=[])
man_node = self.sec2man_get_first_child(section, kernel_doc_man)
name, sect = (man_node["manpage"].split(".", -1) + [DEFAULT_MAN_SECT])[:2]
man_info["manpage"] = name
man_info["mansect"] = sect
# strip field list
field_list = self.sec2man_get_first_child(section, nodes.field_list)
if field_list:
field_list.parent.remove(field_list)
for field in field_list:
name = field[0].astext().lower()
value = field[1].astext()
man_info[name] = man_info.get(name, []) + [
value,
]
# normalize authors
for auth, adr in zip(
man_info.get("author", []), man_info.get("address", [])
):
man_info["authors"].append("%s <%s>" % (auth, adr))
# strip *purpose*
desc_content = self.sec2man_get_first_child(
section, addnodes.desc, addnodes.desc_content
)
if not desc_content or not desc_content:
# missing initial short description in kernel-doc comment
man_info.subtitle = ""
else:
man_info.subtitle = desc_content[0].astext()
del desc_content[0]
# remove section title
old_title = self.sec2man_get_first_child(section, nodes.title)
old_title.parent.remove(old_title)
# gather type of the declaration
decl_type = self.sec2man_get_first_child(
section, addnodes.desc, addnodes.desc_signature, addnodes.desc_type
)
if decl_type is not None:
decl_type = decl_type.astext().strip()
man_info.decl_type = decl_type
# complete infos
man_info.title = man_info["manpage"]
man_info.section = man_info["mansect"]
return man_info
[docs]
def isolate_sections(self, sec_by_title):
section = self.document[0]
while True:
sect = self.sec2man_get_first_child(section, nodes.section)
if not sect:
break
sec_parent = sect.parent
target_idx = sect.parent.index(sect) - 1
sect.parent.remove(sect)
if isinstance(sec_parent[target_idx], nodes.target):
# drop target / is useless in man-pages
del sec_parent[target_idx]
title = sect[0].astext().upper()
for r, man_title in self.manTitles: # pylint: disable=invalid-name
if r.search(title):
title = man_title
sect[0].replace_self(nodes.title(text=title))
break
# we dont know if there are sections with the same title
sec_by_title[title] = sec_by_title.get(title, []) + [sect]
return sec_by_title
[docs]
def isolate_synopsis(self, sec_by_title):
synopsis = None
c_desc = self.sec2man_get_first_child(self.document[0], addnodes.desc)
if c_desc is not None:
c_desc.parent.remove(c_desc)
synopsis = nodes.section()
synopsis += nodes.title(text="synopsis")
synopsis += c_desc
sec_by_title["SYNOPSIS"] = sec_by_title.get("SYNOPSIS", []) + [synopsis]
return sec_by_title
[docs]
def apply(self, **kwargs):
self.document.man_info = self.strip_man_info()
sec_by_title = collections.OrderedDict()
self.isolate_sections(sec_by_title)
# On struct, enum, union, typedef, the SYNOPSIS is taken from the
# DEFINITION section.
if self.document.man_info.decl_type not in [
"struct",
"enum",
"union",
"typedef",
]:
self.isolate_synopsis(sec_by_title)
for sec_name in self.manTitleOrder:
sec_list = sec_by_title.pop(sec_name, [])
self.document[0] += sec_list
for sec_list in sec_by_title.values():
self.document[0] += sec_list
# ==============================================================================
[docs]
class KernelDocManBuilder(ManualPageBuilder):
# ==============================================================================
"""
Builds groff output in manual page format.
"""
name = "kernel-doc-man"
format = "man"
supported_image_types = []
[docs]
def is_manpage(self, node):
if isinstance(node, nodes.section):
return bool(
Section2Manpage.sec2man_get_first_child(node, kernel_doc_man)
is not None
)
return False
[docs]
def prepare_writing(self, docnames):
"""A place where you can add logic before :meth:`write_doc` is run"""
pass
[docs]
def write_doc(self, docname, doctree):
"""Where you actually write something to the filesystem."""
pass
[docs]
def get_partial_document(self, children):
doc_tree = new_document("<output>")
doc_tree += children
return doc_tree
[docs]
def write(self, *ignored): # pylint: disable=overridden-final-method
if self.config.man_pages:
# build manpages from config.man_pages as usual
ManualPageBuilder.write(self, *ignored)
logger.info(
bold("scan master tree for kernel-doc man-pages ... ") + darkgreen("{"),
nonl=True,
)
master_tree = self.env.get_doctree(self.config.master_doc)
master_tree = inline_all_toctrees(
self,
set(),
self.config.master_doc,
master_tree,
darkgreen,
[self.config.master_doc],
)
logger.info(darkgreen("}"))
man_nodes = master_tree.traverse(condition=self.is_manpage)
if not man_nodes and not self.config.man_pages:
logger.warning(
'no "man_pages" config value nor manual section found; no manual pages '
"will be written"
)
return
logger.info(bold("START writing man pages ... "), nonl=True)
for man_parent in man_nodes:
doc_tree = self.get_partial_document(man_parent)
Section2Manpage(doc_tree).apply()
if not doc_tree.man_info["authors"] and self.config.author:
doc_tree.man_info["authors"].append(self.config.author)
doc_writer = ManualPageWriter(self)
doc_settings = OptionParser(
defaults=self.env.settings,
components=(doc_writer,),
read_config_files=True,
).get_default_values()
doc_settings.__dict__.update(doc_tree.man_info)
doc_tree.settings = doc_settings
targetname = "%s.%s" % (doc_tree.man_info.title, doc_tree.man_info.section)
if doc_tree.man_info.decl_type in ["struct", "enum", "union", "typedef"]:
targetname = "%s_%s" % (doc_tree.man_info.decl_type, targetname)
destination = FileOutput(
destination_path=path.join(self.outdir, targetname), encoding="utf-8"
)
logger.info(darkgreen(targetname) + " ", nonl=True)
self.env.resolve_references(doc_tree, doc_tree.man_info.manpage, self)
# remove pending_xref nodes
for pendingnode in doc_tree.traverse(addnodes.pending_xref):
pendingnode.replace_self(pendingnode.children)
doc_writer.write(doc_tree, destination)
logger.info("END writing man pages.")