summaryrefslogtreecommitdiffstats
path: root/sphinx/writers/manpage.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/writers/manpage.py')
-rw-r--r--sphinx/writers/manpage.py473
1 files changed, 473 insertions, 0 deletions
diff --git a/sphinx/writers/manpage.py b/sphinx/writers/manpage.py
new file mode 100644
index 0000000..108eb5e
--- /dev/null
+++ b/sphinx/writers/manpage.py
@@ -0,0 +1,473 @@
+"""Manual page writer, extended for Sphinx custom nodes."""
+
+from __future__ import annotations
+
+from collections.abc import Iterable
+from typing import TYPE_CHECKING, Any, cast
+
+from docutils import nodes
+from docutils.writers.manpage import Translator as BaseTranslator
+from docutils.writers.manpage import Writer
+
+from sphinx import addnodes
+from sphinx.locale import _, admonitionlabels
+from sphinx.util import logging
+from sphinx.util.docutils import SphinxTranslator
+from sphinx.util.i18n import format_date
+from sphinx.util.nodes import NodeMatcher
+
+if TYPE_CHECKING:
+ from docutils.nodes import Element
+
+ from sphinx.builders import Builder
+
+logger = logging.getLogger(__name__)
+
+
+class ManualPageWriter(Writer):
+ def __init__(self, builder: Builder) -> None:
+ super().__init__()
+ self.builder = builder
+
+ def translate(self) -> None:
+ transform = NestedInlineTransform(self.document)
+ transform.apply()
+ visitor = self.builder.create_translator(self.document, self.builder)
+ self.visitor = cast(ManualPageTranslator, visitor)
+ self.document.walkabout(visitor)
+ self.output = self.visitor.astext()
+
+
+class NestedInlineTransform:
+ """
+ Flatten nested inline nodes:
+
+ Before:
+ <strong>foo=<emphasis>1</emphasis>
+ &bar=<emphasis>2</emphasis></strong>
+ After:
+ <strong>foo=</strong><emphasis>var</emphasis>
+ <strong>&bar=</strong><emphasis>2</emphasis>
+ """
+ def __init__(self, document: nodes.document) -> None:
+ self.document = document
+
+ def apply(self, **kwargs: Any) -> None:
+ matcher = NodeMatcher(nodes.literal, nodes.emphasis, nodes.strong)
+ for node in list(self.document.findall(matcher)): # type: nodes.TextElement
+ if any(matcher(subnode) for subnode in node):
+ pos = node.parent.index(node)
+ for subnode in reversed(list(node)):
+ node.remove(subnode)
+ if matcher(subnode):
+ node.parent.insert(pos + 1, subnode)
+ else:
+ newnode = node.__class__('', '', subnode, **node.attributes)
+ node.parent.insert(pos + 1, newnode)
+ # move node if all children became siblings of the node
+ if not len(node):
+ node.parent.remove(node)
+
+
+class ManualPageTranslator(SphinxTranslator, BaseTranslator):
+ """
+ Custom man page translator.
+ """
+
+ _docinfo: dict[str, Any] = {}
+
+ def __init__(self, document: nodes.document, builder: Builder) -> None:
+ super().__init__(document, builder)
+
+ self.in_productionlist = 0
+
+ # first title is the manpage title
+ self.section_level = -1
+
+ # docinfo set by man_pages config value
+ self._docinfo['title'] = self.settings.title
+ self._docinfo['subtitle'] = self.settings.subtitle
+ if self.settings.authors:
+ # don't set it if no author given
+ self._docinfo['author'] = self.settings.authors
+ self._docinfo['manual_section'] = self.settings.section
+
+ # docinfo set by other config values
+ self._docinfo['title_upper'] = self._docinfo['title'].upper()
+ if self.config.today:
+ self._docinfo['date'] = self.config.today
+ else:
+ self._docinfo['date'] = format_date(self.config.today_fmt or _('%b %d, %Y'),
+ language=self.config.language)
+ self._docinfo['copyright'] = self.config.copyright
+ self._docinfo['version'] = self.config.version
+ self._docinfo['manual_group'] = self.config.project
+
+ # Overwrite admonition label translations with our own
+ for label, translation in admonitionlabels.items():
+ self.language.labels[label] = self.deunicode(translation)
+
+ # overwritten -- added quotes around all .TH arguments
+ def header(self) -> str:
+ tmpl = (".TH \"%(title_upper)s\" \"%(manual_section)s\""
+ " \"%(date)s\" \"%(version)s\" \"%(manual_group)s\"\n")
+ if self._docinfo['subtitle']:
+ tmpl += (".SH NAME\n"
+ "%(title)s \\- %(subtitle)s\n")
+ return tmpl % self._docinfo
+
+ def visit_start_of_file(self, node: Element) -> None:
+ pass
+
+ def depart_start_of_file(self, node: Element) -> None:
+ pass
+
+ #############################################################
+ # Domain-specific object descriptions
+ #############################################################
+
+ # Top-level nodes for descriptions
+ ##################################
+
+ def visit_desc(self, node: Element) -> None:
+ self.visit_definition_list(node)
+
+ def depart_desc(self, node: Element) -> None:
+ self.depart_definition_list(node)
+
+ def visit_desc_signature(self, node: Element) -> None:
+ self.visit_definition_list_item(node)
+ self.visit_term(node)
+
+ def depart_desc_signature(self, node: Element) -> None:
+ self.depart_term(node)
+
+ def visit_desc_signature_line(self, node: Element) -> None:
+ pass
+
+ def depart_desc_signature_line(self, node: Element) -> None:
+ self.body.append(' ')
+
+ def visit_desc_content(self, node: Element) -> None:
+ self.visit_definition(node)
+
+ def depart_desc_content(self, node: Element) -> None:
+ self.depart_definition(node)
+
+ def visit_desc_inline(self, node: Element) -> None:
+ pass
+
+ def depart_desc_inline(self, node: Element) -> None:
+ pass
+
+ # Nodes for high-level structure in signatures
+ ##############################################
+
+ def visit_desc_name(self, node: Element) -> None:
+ pass
+
+ def depart_desc_name(self, node: Element) -> None:
+ pass
+
+ def visit_desc_addname(self, node: Element) -> None:
+ pass
+
+ def depart_desc_addname(self, node: Element) -> None:
+ pass
+
+ def visit_desc_type(self, node: Element) -> None:
+ pass
+
+ def depart_desc_type(self, node: Element) -> None:
+ pass
+
+ def visit_desc_returns(self, node: Element) -> None:
+ self.body.append(' -> ')
+
+ def depart_desc_returns(self, node: Element) -> None:
+ pass
+
+ def visit_desc_parameterlist(self, node: Element) -> None:
+ self.body.append('(')
+ self.first_param = 1
+
+ def depart_desc_parameterlist(self, node: Element) -> None:
+ self.body.append(')')
+
+ def visit_desc_type_parameter_list(self, node: Element) -> None:
+ self.body.append('[')
+ self.first_param = 1
+
+ def depart_desc_type_parameter_list(self, node: Element) -> None:
+ self.body.append(']')
+
+ def visit_desc_parameter(self, node: Element) -> None:
+ if not self.first_param:
+ self.body.append(', ')
+ else:
+ self.first_param = 0
+
+ def depart_desc_parameter(self, node: Element) -> None:
+ pass
+
+ def visit_desc_type_parameter(self, node: Element) -> None:
+ self.visit_desc_parameter(node)
+
+ def depart_desc_type_parameter(self, node: Element) -> None:
+ self.depart_desc_parameter(node)
+
+ def visit_desc_optional(self, node: Element) -> None:
+ self.body.append('[')
+
+ def depart_desc_optional(self, node: Element) -> None:
+ self.body.append(']')
+
+ def visit_desc_annotation(self, node: Element) -> None:
+ pass
+
+ def depart_desc_annotation(self, node: Element) -> None:
+ pass
+
+ ##############################################
+
+ def visit_versionmodified(self, node: Element) -> None:
+ self.visit_paragraph(node)
+
+ def depart_versionmodified(self, node: Element) -> None:
+ self.depart_paragraph(node)
+
+ # overwritten -- don't make whole of term bold if it includes strong node
+ def visit_term(self, node: Element) -> None:
+ if any(node.findall(nodes.strong)):
+ self.body.append('\n')
+ else:
+ super().visit_term(node)
+
+ # overwritten -- we don't want source comments to show up
+ def visit_comment(self, node: Element) -> None: # type: ignore[override]
+ raise nodes.SkipNode
+
+ # overwritten -- added ensure_eol()
+ def visit_footnote(self, node: Element) -> None:
+ self.ensure_eol()
+ super().visit_footnote(node)
+
+ # overwritten -- handle footnotes rubric
+ def visit_rubric(self, node: Element) -> None:
+ self.ensure_eol()
+ if len(node) == 1 and node.astext() in ('Footnotes', _('Footnotes')):
+ self.body.append('.SH ' + self.deunicode(node.astext()).upper() + '\n')
+ raise nodes.SkipNode
+ self.body.append('.sp\n')
+
+ def depart_rubric(self, node: Element) -> None:
+ self.body.append('\n')
+
+ def visit_seealso(self, node: Element) -> None:
+ self.visit_admonition(node, 'seealso')
+
+ def depart_seealso(self, node: Element) -> None:
+ self.depart_admonition(node)
+
+ def visit_productionlist(self, node: Element) -> None:
+ self.ensure_eol()
+ names = []
+ self.in_productionlist += 1
+ self.body.append('.sp\n.nf\n')
+ productionlist = cast(Iterable[addnodes.production], node)
+ for production in productionlist:
+ names.append(production['tokenname'])
+ maxlen = max(len(name) for name in names)
+ lastname = None
+ for production in productionlist:
+ if production['tokenname']:
+ lastname = production['tokenname'].ljust(maxlen)
+ self.body.append(self.defs['strong'][0])
+ self.body.append(self.deunicode(lastname))
+ self.body.append(self.defs['strong'][1])
+ self.body.append(' ::= ')
+ elif lastname is not None:
+ self.body.append('%s ' % (' ' * len(lastname)))
+ production.walkabout(self)
+ self.body.append('\n')
+ self.body.append('\n.fi\n')
+ self.in_productionlist -= 1
+ raise nodes.SkipNode
+
+ def visit_production(self, node: Element) -> None:
+ pass
+
+ def depart_production(self, node: Element) -> None:
+ pass
+
+ # overwritten -- don't emit a warning for images
+ def visit_image(self, node: Element) -> None:
+ if 'alt' in node.attributes:
+ self.body.append(_('[image: %s]') % node['alt'] + '\n')
+ self.body.append(_('[image]') + '\n')
+ raise nodes.SkipNode
+
+ # overwritten -- don't visit inner marked up nodes
+ def visit_reference(self, node: Element) -> None:
+ self.body.append(self.defs['reference'][0])
+ # avoid repeating escaping code... fine since
+ # visit_Text calls astext() and only works on that afterwards
+ self.visit_Text(node) # type: ignore[arg-type]
+ self.body.append(self.defs['reference'][1])
+
+ uri = node.get('refuri', '')
+ if uri.startswith(('mailto:', 'http:', 'https:', 'ftp:')):
+ # if configured, put the URL after the link
+ if self.config.man_show_urls and node.astext() != uri:
+ if uri.startswith('mailto:'):
+ uri = uri[7:]
+ self.body.extend([
+ ' <',
+ self.defs['strong'][0], uri, self.defs['strong'][1],
+ '>'])
+ raise nodes.SkipNode
+
+ def visit_number_reference(self, node: Element) -> None:
+ text = nodes.Text(node.get('title', '#'))
+ self.visit_Text(text)
+ raise nodes.SkipNode
+
+ def visit_centered(self, node: Element) -> None:
+ self.ensure_eol()
+ self.body.append('.sp\n.ce\n')
+
+ def depart_centered(self, node: Element) -> None:
+ self.body.append('\n.ce 0\n')
+
+ def visit_compact_paragraph(self, node: Element) -> None:
+ pass
+
+ def depart_compact_paragraph(self, node: Element) -> None:
+ pass
+
+ def visit_download_reference(self, node: Element) -> None:
+ pass
+
+ def depart_download_reference(self, node: Element) -> None:
+ pass
+
+ def visit_toctree(self, node: Element) -> None:
+ raise nodes.SkipNode
+
+ def visit_index(self, node: Element) -> None:
+ raise nodes.SkipNode
+
+ def visit_tabular_col_spec(self, node: Element) -> None:
+ raise nodes.SkipNode
+
+ def visit_glossary(self, node: Element) -> None:
+ pass
+
+ def depart_glossary(self, node: Element) -> None:
+ pass
+
+ def visit_acks(self, node: Element) -> None:
+ bullet_list = cast(nodes.bullet_list, node[0])
+ list_items = cast(Iterable[nodes.list_item], bullet_list)
+ self.ensure_eol()
+ bullet_list = cast(nodes.bullet_list, node[0])
+ list_items = cast(Iterable[nodes.list_item], bullet_list)
+ self.body.append(', '.join(n.astext() for n in list_items) + '.')
+ self.body.append('\n')
+ raise nodes.SkipNode
+
+ def visit_hlist(self, node: Element) -> None:
+ self.visit_bullet_list(node)
+
+ def depart_hlist(self, node: Element) -> None:
+ self.depart_bullet_list(node)
+
+ def visit_hlistcol(self, node: Element) -> None:
+ pass
+
+ def depart_hlistcol(self, node: Element) -> None:
+ pass
+
+ def visit_literal_emphasis(self, node: Element) -> None:
+ return self.visit_emphasis(node)
+
+ def depart_literal_emphasis(self, node: Element) -> None:
+ return self.depart_emphasis(node)
+
+ def visit_literal_strong(self, node: Element) -> None:
+ return self.visit_strong(node)
+
+ def depart_literal_strong(self, node: Element) -> None:
+ return self.depart_strong(node)
+
+ def visit_abbreviation(self, node: Element) -> None:
+ pass
+
+ def depart_abbreviation(self, node: Element) -> None:
+ pass
+
+ def visit_manpage(self, node: Element) -> None:
+ return self.visit_strong(node)
+
+ def depart_manpage(self, node: Element) -> None:
+ return self.depart_strong(node)
+
+ # overwritten: handle section titles better than in 0.6 release
+ def visit_caption(self, node: Element) -> None:
+ if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'):
+ self.body.append('.sp\n')
+ else:
+ super().visit_caption(node)
+
+ def depart_caption(self, node: Element) -> None:
+ if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'):
+ self.body.append('\n')
+ else:
+ super().depart_caption(node)
+
+ # overwritten: handle section titles better than in 0.6 release
+ def visit_title(self, node: Element) -> None:
+ if isinstance(node.parent, addnodes.seealso):
+ self.body.append('.IP "')
+ return None
+ elif isinstance(node.parent, nodes.section):
+ if self.section_level == 0:
+ # skip the document title
+ raise nodes.SkipNode
+ elif self.section_level == 1:
+ self.body.append('.SH %s\n' %
+ self.deunicode(node.astext().upper()))
+ raise nodes.SkipNode
+ return super().visit_title(node)
+
+ def depart_title(self, node: Element) -> None:
+ if isinstance(node.parent, addnodes.seealso):
+ self.body.append('"\n')
+ return None
+ return super().depart_title(node)
+
+ def visit_raw(self, node: Element) -> None:
+ if 'manpage' in node.get('format', '').split():
+ self.body.append(node.astext())
+ raise nodes.SkipNode
+
+ def visit_meta(self, node: Element) -> None:
+ raise nodes.SkipNode
+
+ def visit_inline(self, node: Element) -> None:
+ pass
+
+ def depart_inline(self, node: Element) -> None:
+ pass
+
+ def visit_math(self, node: Element) -> None:
+ pass
+
+ def depart_math(self, node: Element) -> None:
+ pass
+
+ def visit_math_block(self, node: Element) -> None:
+ self.visit_centered(node)
+
+ def depart_math_block(self, node: Element) -> None:
+ self.depart_centered(node)