From cf7da1843c45a4c2df7a749f7886a2d2ba0ee92a Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Mon, 15 Apr 2024 19:25:40 +0200 Subject: Adding upstream version 7.2.6. Signed-off-by: Daniel Baumann --- sphinx/writers/texinfo.py | 1572 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1572 insertions(+) create mode 100644 sphinx/writers/texinfo.py (limited to 'sphinx/writers/texinfo.py') diff --git a/sphinx/writers/texinfo.py b/sphinx/writers/texinfo.py new file mode 100644 index 0000000..7032c65 --- /dev/null +++ b/sphinx/writers/texinfo.py @@ -0,0 +1,1572 @@ +"""Custom docutils writer for Texinfo.""" + +from __future__ import annotations + +import re +import textwrap +from collections.abc import Iterable, Iterator +from os import path +from typing import TYPE_CHECKING, Any, cast + +from docutils import nodes, writers + +from sphinx import __display_version__, addnodes +from sphinx.domains.index import IndexDomain +from sphinx.errors import ExtensionError +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.writers.latex import collected_footnote + +if TYPE_CHECKING: + from docutils.nodes import Element, Node, Text + + from sphinx.builders.texinfo import TexinfoBuilder + from sphinx.domains import IndexEntry + + +logger = logging.getLogger(__name__) + + +COPYING = """\ +@quotation +%(project)s %(release)s, %(date)s + +%(author)s + +Copyright @copyright{} %(copyright)s +@end quotation +""" + +TEMPLATE = """\ +\\input texinfo @c -*-texinfo-*- +@c %%**start of header +@setfilename %(filename)s +@documentencoding UTF-8 +@ifinfo +@*Generated by Sphinx """ + __display_version__ + """.@* +@end ifinfo +@settitle %(title)s +@defindex ge +@paragraphindent %(paragraphindent)s +@exampleindent %(exampleindent)s +@finalout +%(direntry)s +@c %%**end of header + +@copying +%(copying)s +@end copying + +@titlepage +@title %(title)s +@insertcopying +@end titlepage +@contents + +@c %%** start of user preamble +%(preamble)s +@c %%** end of user preamble + +@ifnottex +@node Top +@top %(title)s +@insertcopying +@end ifnottex + +@c %%**start of body +%(body)s +@c %%**end of body +@bye +""" + + +def find_subsections(section: Element) -> list[nodes.section]: + """Return a list of subsections for the given ``section``.""" + result = [] + for child in section: + if isinstance(child, nodes.section): + result.append(child) + continue + if isinstance(child, nodes.Element): + result.extend(find_subsections(child)) + return result + + +def smart_capwords(s: str, sep: str | None = None) -> str: + """Like string.capwords() but does not capitalize words that already + contain a capital letter.""" + words = s.split(sep) + for i, word in enumerate(words): + if all(x.islower() for x in word): + words[i] = word.capitalize() + return (sep or ' ').join(words) + + +class TexinfoWriter(writers.Writer): + """Texinfo writer for generating Texinfo documents.""" + supported = ('texinfo', 'texi') + + settings_spec: tuple[str, Any, tuple[tuple[str, list[str], dict[str, str]], ...]] = ( + 'Texinfo Specific Options', None, ( + ("Name of the Info file", ['--texinfo-filename'], {'default': ''}), + ('Dir entry', ['--texinfo-dir-entry'], {'default': ''}), + ('Description', ['--texinfo-dir-description'], {'default': ''}), + ('Category', ['--texinfo-dir-category'], {'default': + 'Miscellaneous'}))) + + settings_defaults: dict[str, Any] = {} + + output: str + + visitor_attributes = ('output', 'fragment') + + def __init__(self, builder: TexinfoBuilder) -> None: + super().__init__() + self.builder = builder + + def translate(self) -> None: + visitor = self.builder.create_translator(self.document, self.builder) + self.visitor = cast(TexinfoTranslator, visitor) + self.document.walkabout(visitor) + self.visitor.finish() + for attr in self.visitor_attributes: + setattr(self, attr, getattr(self.visitor, attr)) + + +class TexinfoTranslator(SphinxTranslator): + + ignore_missing_images = False + builder: TexinfoBuilder + + default_elements = { + 'author': '', + 'body': '', + 'copying': '', + 'date': '', + 'direntry': '', + 'exampleindent': 4, + 'filename': '', + 'paragraphindent': 0, + 'preamble': '', + 'project': '', + 'release': '', + 'title': '', + } + + def __init__(self, document: nodes.document, builder: TexinfoBuilder) -> None: + super().__init__(document, builder) + self.init_settings() + + self.written_ids: set[str] = set() # node names and anchors in output + # node names and anchors that should be in output + self.referenced_ids: set[str] = set() + self.indices: list[tuple[str, str]] = [] # (node name, content) + self.short_ids: dict[str, str] = {} # anchors --> short ids + self.node_names: dict[str, str] = {} # node name --> node's name to display + self.node_menus: dict[str, list[str]] = {} # node name --> node's menu entries + self.rellinks: dict[str, list[str]] = {} # node name --> (next, previous, up) + + self.collect_indices() + self.collect_node_names() + self.collect_node_menus() + self.collect_rellinks() + + self.body: list[str] = [] + self.context: list[str] = [] + self.descs: list[addnodes.desc] = [] + self.previous_section: nodes.section | None = None + self.section_level = 0 + self.seen_title = False + self.next_section_ids: set[str] = set() + self.escape_newlines = 0 + self.escape_hyphens = 0 + self.curfilestack: list[str] = [] + self.footnotestack: list[dict[str, list[collected_footnote | bool]]] = [] + self.in_footnote = 0 + self.in_samp = 0 + self.handled_abbrs: set[str] = set() + self.colwidths: list[int] = [] + + def finish(self) -> None: + if self.previous_section is None: + self.add_menu('Top') + for index in self.indices: + name, content = index + pointers = tuple([name] + self.rellinks[name]) + self.body.append('\n@node %s,%s,%s,%s\n' % pointers) + self.body.append(f'@unnumbered {name}\n\n{content}\n') + + while self.referenced_ids: + # handle xrefs with missing anchors + r = self.referenced_ids.pop() + if r not in self.written_ids: + self.body.append('@anchor{{{}}}@w{{{}}}\n'.format(r, ' ' * 30)) + self.ensure_eol() + self.fragment = ''.join(self.body) + self.elements['body'] = self.fragment + self.output = TEMPLATE % self.elements + + # -- Helper routines + + def init_settings(self) -> None: + elements = self.elements = self.default_elements.copy() + elements.update({ + # if empty, the title is set to the first section title + 'title': self.settings.title, + 'author': self.settings.author, + # if empty, use basename of input file + 'filename': self.settings.texinfo_filename, + 'release': self.escape(self.config.release), + 'project': self.escape(self.config.project), + 'copyright': self.escape(self.config.copyright), + 'date': self.escape(self.config.today or + format_date(self.config.today_fmt or _('%b %d, %Y'), + language=self.config.language)), + }) + # title + title: str = self.settings.title + if not title: + title_node = self.document.next_node(nodes.title) + title = title_node.astext() if title_node else '' + elements['title'] = self.escape_id(title) or '' + # filename + if not elements['filename']: + elements['filename'] = self.document.get('source') or 'untitled' + if elements['filename'][-4:] in ('.txt', '.rst'): # type: ignore[index] + elements['filename'] = elements['filename'][:-4] # type: ignore[index] + elements['filename'] += '.info' # type: ignore[operator] + # direntry + if self.settings.texinfo_dir_entry: + entry = self.format_menu_entry( + self.escape_menu(self.settings.texinfo_dir_entry), + '(%s)' % elements['filename'], + self.escape_arg(self.settings.texinfo_dir_description)) + elements['direntry'] = ('@dircategory %s\n' + '@direntry\n' + '%s' + '@end direntry\n') % ( + self.escape_id(self.settings.texinfo_dir_category), entry) + elements['copying'] = COPYING % elements + # allow the user to override them all + elements.update(self.settings.texinfo_elements) + + def collect_node_names(self) -> None: + """Generates a unique id for each section. + + Assigns the attribute ``node_name`` to each section.""" + + def add_node_name(name: str) -> str: + node_id = self.escape_id(name) + nth, suffix = 1, '' + while node_id + suffix in self.written_ids or \ + node_id + suffix in self.node_names: + nth += 1 + suffix = '<%s>' % nth + node_id += suffix + self.written_ids.add(node_id) + self.node_names[node_id] = name + return node_id + + # must have a "Top" node + self.document['node_name'] = 'Top' + add_node_name('Top') + add_node_name('top') + # each index is a node + self.indices = [(add_node_name(name), content) + for name, content in self.indices] + # each section is also a node + for section in self.document.findall(nodes.section): + title = cast(nodes.TextElement, section.next_node(nodes.Titular)) + name = title.astext() if title else '' + section['node_name'] = add_node_name(name) + + def collect_node_menus(self) -> None: + """Collect the menu entries for each "node" section.""" + node_menus = self.node_menus + targets: list[Element] = [self.document] + targets.extend(self.document.findall(nodes.section)) + for node in targets: + assert 'node_name' in node and node['node_name'] # NoQA: PT018 + entries = [s['node_name'] for s in find_subsections(node)] + node_menus[node['node_name']] = entries + # try to find a suitable "Top" node + title = self.document.next_node(nodes.title) + top = title.parent if title else self.document + if not isinstance(top, (nodes.document, nodes.section)): + top = self.document + if top is not self.document: + entries = node_menus[top['node_name']] + entries += node_menus['Top'][1:] + node_menus['Top'] = entries + del node_menus[top['node_name']] + top['node_name'] = 'Top' + # handle the indices + for name, _content in self.indices: + node_menus[name] = [] + node_menus['Top'].append(name) + + def collect_rellinks(self) -> None: + """Collect the relative links (next, previous, up) for each "node".""" + rellinks = self.rellinks + node_menus = self.node_menus + for id in node_menus: + rellinks[id] = ['', '', ''] + # up's + for id, entries in node_menus.items(): + for e in entries: + rellinks[e][2] = id + # next's and prev's + for id, entries in node_menus.items(): + for i, id in enumerate(entries): + # First child's prev is empty + if i != 0: + rellinks[id][1] = entries[i - 1] + # Last child's next is empty + if i != len(entries) - 1: + rellinks[id][0] = entries[i + 1] + # top's next is its first child + try: + first = node_menus['Top'][0] + except IndexError: + pass + else: + rellinks['Top'][0] = first + rellinks[first][1] = 'Top' + + # -- Escaping + # Which characters to escape depends on the context. In some cases, + # namely menus and node names, it's not possible to escape certain + # characters. + + def escape(self, s: str) -> str: + """Return a string with Texinfo command characters escaped.""" + s = s.replace('@', '@@') + s = s.replace('{', '@{') + s = s.replace('}', '@}') + # prevent `` and '' quote conversion + s = s.replace('``', "`@w{`}") + s = s.replace("''", "'@w{'}") + return s + + def escape_arg(self, s: str) -> str: + """Return an escaped string suitable for use as an argument + to a Texinfo command.""" + s = self.escape(s) + # commas are the argument delimiters + s = s.replace(',', '@comma{}') + # normalize white space + s = ' '.join(s.split()).strip() + return s + + def escape_id(self, s: str) -> str: + """Return an escaped string suitable for node names and anchors.""" + bad_chars = ',:()' + for bc in bad_chars: + s = s.replace(bc, ' ') + if re.search('[^ .]', s): + # remove DOTs if name contains other characters + s = s.replace('.', ' ') + s = ' '.join(s.split()).strip() + return self.escape(s) + + def escape_menu(self, s: str) -> str: + """Return an escaped string suitable for menu entries.""" + s = self.escape_arg(s) + s = s.replace(':', ';') + s = ' '.join(s.split()).strip() + return s + + def ensure_eol(self) -> None: + """Ensure the last line in body is terminated by new line.""" + if self.body and self.body[-1][-1:] != '\n': + self.body.append('\n') + + def format_menu_entry(self, name: str, node_name: str, desc: str) -> str: + if name == node_name: + s = f'* {name}:: ' + else: + s = f'* {name}: {node_name}. ' + offset = max((24, (len(name) + 4) % 78)) + wdesc = '\n'.join(' ' * offset + l for l in + textwrap.wrap(desc, width=78 - offset)) + return s + wdesc.strip() + '\n' + + def add_menu_entries( + self, + entries: list[str], + reg: re.Pattern[str] = re.compile(r'\s+---?\s+'), + ) -> None: + for entry in entries: + name = self.node_names[entry] + # special formatting for entries that are divided by an em-dash + try: + parts = reg.split(name, 1) + except TypeError: + # could be a gettext proxy + parts = [name] + if len(parts) == 2: + name, desc = parts + else: + desc = '' + name = self.escape_menu(name) + desc = self.escape(desc) + self.body.append(self.format_menu_entry(name, entry, desc)) + + def add_menu(self, node_name: str) -> None: + entries = self.node_menus[node_name] + if not entries: + return + self.body.append('\n@menu\n') + self.add_menu_entries(entries) + if (node_name != 'Top' or + not self.node_menus[entries[0]] or + self.config.texinfo_no_detailmenu): + self.body.append('\n@end menu\n') + return + + def _add_detailed_menu(name: str) -> None: + entries = self.node_menus[name] + if not entries: + return + self.body.append(f'\n{self.escape(self.node_names[name], )}\n\n') + self.add_menu_entries(entries) + for subentry in entries: + _add_detailed_menu(subentry) + + self.body.append('\n@detailmenu\n' + ' --- The Detailed Node Listing ---\n') + for entry in entries: + _add_detailed_menu(entry) + self.body.append('\n@end detailmenu\n' + '@end menu\n') + + def tex_image_length(self, width_str: str) -> str: + match = re.match(r'(\d*\.?\d*)\s*(\S*)', width_str) + if not match: + # fallback + return width_str + res = width_str + amount, unit = match.groups()[:2] + if not unit or unit == "px": + # pixels: let TeX alone + return '' + elif unit == "%": + # a4paper: textwidth=418.25368pt + res = "%d.0pt" % (float(amount) * 4.1825368) + return res + + def collect_indices(self) -> None: + def generate(content: list[tuple[str, list[IndexEntry]]], collapsed: bool) -> str: + ret = ['\n@menu\n'] + for _letter, entries in content: + for entry in entries: + if not entry[3]: + continue + name = self.escape_menu(entry[0]) + sid = self.get_short_id(f'{entry[2]}:{entry[3]}') + desc = self.escape_arg(entry[6]) + me = self.format_menu_entry(name, sid, desc) + ret.append(me) + ret.append('@end menu\n') + return ''.join(ret) + + indices_config = self.config.texinfo_domain_indices + if indices_config: + for domain in self.builder.env.domains.values(): + for indexcls in domain.indices: + indexname = f'{domain.name}-{indexcls.name}' + if isinstance(indices_config, list): + if indexname not in indices_config: + continue + content, collapsed = indexcls(domain).generate( + self.builder.docnames) + if not content: + continue + self.indices.append((indexcls.localname, + generate(content, collapsed))) + # only add the main Index if it's not empty + domain = cast(IndexDomain, self.builder.env.get_domain('index')) + for docname in self.builder.docnames: + if domain.entries[docname]: + self.indices.append((_('Index'), '\n@printindex ge\n')) + break + + # this is copied from the latex writer + # TODO: move this to sphinx.util + + def collect_footnotes( + self, node: Element, + ) -> dict[str, list[collected_footnote | bool]]: + def footnotes_under(n: Element) -> Iterator[nodes.footnote]: + if isinstance(n, nodes.footnote): + yield n + else: + for c in n.children: + if isinstance(c, addnodes.start_of_file): + continue + elif isinstance(c, nodes.Element): + yield from footnotes_under(c) + fnotes: dict[str, list[collected_footnote | bool]] = {} + for fn in footnotes_under(node): + label = cast(nodes.label, fn[0]) + num = label.astext().strip() + fnotes[num] = [collected_footnote('', *fn.children), False] + return fnotes + + # -- xref handling + + def get_short_id(self, id: str) -> str: + """Return a shorter 'id' associated with ``id``.""" + # Shorter ids improve paragraph filling in places + # that the id is hidden by Emacs. + try: + sid = self.short_ids[id] + except KeyError: + sid = hex(len(self.short_ids))[2:] + self.short_ids[id] = sid + return sid + + def add_anchor(self, id: str, node: Node) -> None: + if id.startswith('index-'): + return + id = self.curfilestack[-1] + ':' + id + eid = self.escape_id(id) + sid = self.get_short_id(id) + for id in (eid, sid): + if id not in self.written_ids: + self.body.append('@anchor{%s}' % id) + self.written_ids.add(id) + + def add_xref(self, id: str, name: str, node: Node) -> None: + name = self.escape_menu(name) + sid = self.get_short_id(id) + if self.config.texinfo_cross_references: + self.body.append(f'@ref{{{sid},,{name}}}') + self.referenced_ids.add(sid) + self.referenced_ids.add(self.escape_id(id)) + else: + self.body.append(name) + + # -- Visiting + + def visit_document(self, node: Element) -> None: + self.footnotestack.append(self.collect_footnotes(node)) + self.curfilestack.append(node.get('docname', '')) + if 'docname' in node: + self.add_anchor(':doc', node) + + def depart_document(self, node: Element) -> None: + self.footnotestack.pop() + self.curfilestack.pop() + + def visit_Text(self, node: Text) -> None: + s = self.escape(node.astext()) + if self.escape_newlines: + s = s.replace('\n', ' ') + if self.escape_hyphens: + # prevent "--" and "---" conversion + s = s.replace('-', '@w{-}') + self.body.append(s) + + def depart_Text(self, node: Text) -> None: + pass + + def visit_section(self, node: Element) -> None: + self.next_section_ids.update(node.get('ids', [])) + if not self.seen_title: + return + if self.previous_section: + self.add_menu(self.previous_section['node_name']) + else: + self.add_menu('Top') + + node_name = node['node_name'] + pointers = tuple([node_name] + self.rellinks[node_name]) + self.body.append('\n@node %s,%s,%s,%s\n' % pointers) + for id in sorted(self.next_section_ids): + self.add_anchor(id, node) + + self.next_section_ids.clear() + self.previous_section = cast(nodes.section, node) + self.section_level += 1 + + def depart_section(self, node: Element) -> None: + self.section_level -= 1 + + headings = ( + '@unnumbered', + '@chapter', + '@section', + '@subsection', + '@subsubsection', + ) + + rubrics = ( + '@heading', + '@subheading', + '@subsubheading', + ) + + def visit_title(self, node: Element) -> None: + if not self.seen_title: + self.seen_title = True + raise nodes.SkipNode + parent = node.parent + if isinstance(parent, nodes.table): + return + if isinstance(parent, (nodes.Admonition, nodes.sidebar, nodes.topic)): + raise nodes.SkipNode + if not isinstance(parent, nodes.section): + logger.warning(__('encountered title node not in section, topic, table, ' + 'admonition or sidebar'), + location=node) + self.visit_rubric(node) + else: + try: + heading = self.headings[self.section_level] + except IndexError: + heading = self.headings[-1] + self.body.append('\n%s ' % heading) + + def depart_title(self, node: Element) -> None: + self.body.append('\n\n') + + def visit_rubric(self, node: Element) -> None: + if len(node) == 1 and node.astext() in ('Footnotes', _('Footnotes')): + raise nodes.SkipNode + try: + rubric = self.rubrics[self.section_level] + except IndexError: + rubric = self.rubrics[-1] + self.body.append('\n%s ' % rubric) + self.escape_newlines += 1 + + def depart_rubric(self, node: Element) -> None: + self.escape_newlines -= 1 + self.body.append('\n\n') + + def visit_subtitle(self, node: Element) -> None: + self.body.append('\n\n@noindent\n') + + def depart_subtitle(self, node: Element) -> None: + self.body.append('\n\n') + + # -- References + + def visit_target(self, node: Element) -> None: + # postpone the labels until after the sectioning command + parindex = node.parent.index(node) + try: + try: + next = node.parent[parindex + 1] + except IndexError: + # last node in parent, look at next after parent + # (for section of equal level) + next = node.parent.parent[node.parent.parent.index(node.parent)] + if isinstance(next, nodes.section): + if node.get('refid'): + self.next_section_ids.add(node['refid']) + self.next_section_ids.update(node['ids']) + return + except (IndexError, AttributeError): + pass + if 'refuri' in node: + return + if node.get('refid'): + self.add_anchor(node['refid'], node) + for id in node['ids']: + self.add_anchor(id, node) + + def depart_target(self, node: Element) -> None: + pass + + def visit_reference(self, node: Element) -> None: + # an xref's target is displayed in Info so we ignore a few + # cases for the sake of appearance + if isinstance(node.parent, (nodes.title, addnodes.desc_type)): + return + if isinstance(node[0], nodes.image): + return + name = node.get('name', node.astext()).strip() + uri = node.get('refuri', '') + if not uri and node.get('refid'): + uri = '%' + self.curfilestack[-1] + '#' + node['refid'] + if not uri: + return + if uri.startswith('mailto:'): + uri = self.escape_arg(uri[7:]) + name = self.escape_arg(name) + if not name or name == uri: + self.body.append('@email{%s}' % uri) + else: + self.body.append(f'@email{{{uri},{name}}}') + elif uri.startswith('#'): + # references to labels in the same document + id = self.curfilestack[-1] + ':' + uri[1:] + self.add_xref(id, name, node) + elif uri.startswith('%'): + # references to documents or labels inside documents + hashindex = uri.find('#') + if hashindex == -1: + # reference to the document + id = uri[1:] + '::doc' + else: + # reference to a label + id = uri[1:].replace('#', ':') + self.add_xref(id, name, node) + elif uri.startswith('info:'): + # references to an external Info file + uri = uri[5:].replace('_', ' ') + uri = self.escape_arg(uri) + id = 'Top' + if '#' in uri: + uri, id = uri.split('#', 1) + id = self.escape_id(id) + name = self.escape_menu(name) + if name == id: + self.body.append(f'@ref{{{id},,,{uri}}}') + else: + self.body.append(f'@ref{{{id},,{name},{uri}}}') + else: + uri = self.escape_arg(uri) + name = self.escape_arg(name) + show_urls = self.config.texinfo_show_urls + if self.in_footnote: + show_urls = 'inline' + if not name or uri == name: + self.body.append('@indicateurl{%s}' % uri) + elif show_urls == 'inline': + self.body.append(f'@uref{{{uri},{name}}}') + elif show_urls == 'no': + self.body.append(f'@uref{{{uri},,{name}}}') + else: + self.body.append(f'{name}@footnote{{{uri}}}') + raise nodes.SkipNode + + def depart_reference(self, node: Element) -> None: + pass + + def visit_number_reference(self, node: Element) -> None: + text = nodes.Text(node.get('title', '#')) + self.visit_Text(text) + raise nodes.SkipNode + + def visit_title_reference(self, node: Element) -> None: + text = node.astext() + self.body.append('@cite{%s}' % self.escape_arg(text)) + raise nodes.SkipNode + + # -- Blocks + + def visit_paragraph(self, node: Element) -> None: + self.body.append('\n') + + def depart_paragraph(self, node: Element) -> None: + self.body.append('\n') + + def visit_block_quote(self, node: Element) -> None: + self.body.append('\n@quotation\n') + + def depart_block_quote(self, node: Element) -> None: + self.ensure_eol() + self.body.append('@end quotation\n') + + def visit_literal_block(self, node: Element | None) -> None: + self.body.append('\n@example\n') + + def depart_literal_block(self, node: Element | None) -> None: + self.ensure_eol() + self.body.append('@end example\n') + + visit_doctest_block = visit_literal_block + depart_doctest_block = depart_literal_block + + def visit_line_block(self, node: Element) -> None: + if not isinstance(node.parent, nodes.line_block): + self.body.append('\n\n') + self.body.append('@display\n') + + def depart_line_block(self, node: Element) -> None: + self.body.append('@end display\n') + if not isinstance(node.parent, nodes.line_block): + self.body.append('\n\n') + + def visit_line(self, node: Element) -> None: + self.escape_newlines += 1 + + def depart_line(self, node: Element) -> None: + self.body.append('@w{ }\n') + self.escape_newlines -= 1 + + # -- Inline + + def visit_strong(self, node: Element) -> None: + self.body.append('`') + + def depart_strong(self, node: Element) -> None: + self.body.append("'") + + def visit_emphasis(self, node: Element) -> None: + if self.in_samp: + self.body.append('@var{') + self.context.append('}') + else: + self.body.append('`') + self.context.append("'") + + def depart_emphasis(self, node: Element) -> None: + self.body.append(self.context.pop()) + + def is_samp(self, node: Element) -> bool: + return 'samp' in node['classes'] + + def visit_literal(self, node: Element) -> None: + if self.is_samp(node): + self.in_samp += 1 + self.body.append('@code{') + + def depart_literal(self, node: Element) -> None: + if self.is_samp(node): + self.in_samp -= 1 + self.body.append('}') + + def visit_superscript(self, node: Element) -> None: + self.body.append('@w{^') + + def depart_superscript(self, node: Element) -> None: + self.body.append('}') + + def visit_subscript(self, node: Element) -> None: + self.body.append('@w{[') + + def depart_subscript(self, node: Element) -> None: + self.body.append(']}') + + # -- Footnotes + + def visit_footnote(self, node: Element) -> None: + raise nodes.SkipNode + + def visit_collected_footnote(self, node: Element) -> None: + self.in_footnote += 1 + self.body.append('@footnote{') + + def depart_collected_footnote(self, node: Element) -> None: + self.body.append('}') + self.in_footnote -= 1 + + def visit_footnote_reference(self, node: Element) -> None: + num = node.astext().strip() + try: + footnode, used = self.footnotestack[-1][num] + except (KeyError, IndexError) as exc: + raise nodes.SkipNode from exc + # footnotes are repeated for each reference + footnode.walkabout(self) # type: ignore[union-attr] + raise nodes.SkipChildren + + def visit_citation(self, node: Element) -> None: + self.body.append('\n') + for id in node.get('ids'): + self.add_anchor(id, node) + self.escape_newlines += 1 + + def depart_citation(self, node: Element) -> None: + self.escape_newlines -= 1 + + def visit_citation_reference(self, node: Element) -> None: + self.body.append('@w{[') + + def depart_citation_reference(self, node: Element) -> None: + self.body.append(']}') + + # -- Lists + + def visit_bullet_list(self, node: Element) -> None: + bullet = node.get('bullet', '*') + self.body.append('\n\n@itemize %s\n' % bullet) + + def depart_bullet_list(self, node: Element) -> None: + self.ensure_eol() + self.body.append('@end itemize\n') + + def visit_enumerated_list(self, node: Element) -> None: + # doesn't support Roman numerals + enum = node.get('enumtype', 'arabic') + starters = {'arabic': '', + 'loweralpha': 'a', + 'upperalpha': 'A'} + start = node.get('start', starters.get(enum, '')) + self.body.append('\n\n@enumerate %s\n' % start) + + def depart_enumerated_list(self, node: Element) -> None: + self.ensure_eol() + self.body.append('@end enumerate\n') + + def visit_list_item(self, node: Element) -> None: + self.body.append('\n@item ') + + def depart_list_item(self, node: Element) -> None: + pass + + # -- Option List + + def visit_option_list(self, node: Element) -> None: + self.body.append('\n\n@table @option\n') + + def depart_option_list(self, node: Element) -> None: + self.ensure_eol() + self.body.append('@end table\n') + + def visit_option_list_item(self, node: Element) -> None: + pass + + def depart_option_list_item(self, node: Element) -> None: + pass + + def visit_option_group(self, node: Element) -> None: + self.at_item_x = '@item' + + def depart_option_group(self, node: Element) -> None: + pass + + def visit_option(self, node: Element) -> None: + self.escape_hyphens += 1 + self.body.append('\n%s ' % self.at_item_x) + self.at_item_x = '@itemx' + + def depart_option(self, node: Element) -> None: + self.escape_hyphens -= 1 + + def visit_option_string(self, node: Element) -> None: + pass + + def depart_option_string(self, node: Element) -> None: + pass + + def visit_option_argument(self, node: Element) -> None: + self.body.append(node.get('delimiter', ' ')) + + def depart_option_argument(self, node: Element) -> None: + pass + + def visit_description(self, node: Element) -> None: + self.body.append('\n') + + def depart_description(self, node: Element) -> None: + pass + + # -- Definitions + + def visit_definition_list(self, node: Element) -> None: + self.body.append('\n\n@table @asis\n') + + def depart_definition_list(self, node: Element) -> None: + self.ensure_eol() + self.body.append('@end table\n') + + def visit_definition_list_item(self, node: Element) -> None: + self.at_item_x = '@item' + + def depart_definition_list_item(self, node: Element) -> None: + pass + + def visit_term(self, node: Element) -> None: + for id in node.get('ids'): + self.add_anchor(id, node) + # anchors and indexes need to go in front + for n in node[::]: + if isinstance(n, (addnodes.index, nodes.target)): + n.walkabout(self) + node.remove(n) + self.body.append('\n%s ' % self.at_item_x) + self.at_item_x = '@itemx' + + def depart_term(self, node: Element) -> None: + pass + + def visit_classifier(self, node: Element) -> None: + self.body.append(' : ') + + def depart_classifier(self, node: Element) -> None: + pass + + def visit_definition(self, node: Element) -> None: + self.body.append('\n') + + def depart_definition(self, node: Element) -> None: + pass + + # -- Tables + + def visit_table(self, node: Element) -> None: + self.entry_sep = '@item' + + def depart_table(self, node: Element) -> None: + self.body.append('\n@end multitable\n\n') + + def visit_tabular_col_spec(self, node: Element) -> None: + pass + + def depart_tabular_col_spec(self, node: Element) -> None: + pass + + def visit_colspec(self, node: Element) -> None: + self.colwidths.append(node['colwidth']) + if len(self.colwidths) != self.n_cols: + return + self.body.append('\n\n@multitable ') + for n in self.colwidths: + self.body.append('{%s} ' % ('x' * (n + 2))) + + def depart_colspec(self, node: Element) -> None: + pass + + def visit_tgroup(self, node: Element) -> None: + self.colwidths = [] + self.n_cols = node['cols'] + + def depart_tgroup(self, node: Element) -> None: + pass + + def visit_thead(self, node: Element) -> None: + self.entry_sep = '@headitem' + + def depart_thead(self, node: Element) -> None: + pass + + def visit_tbody(self, node: Element) -> None: + pass + + def depart_tbody(self, node: Element) -> None: + pass + + def visit_row(self, node: Element) -> None: + pass + + def depart_row(self, node: Element) -> None: + self.entry_sep = '@item' + + def visit_entry(self, node: Element) -> None: + self.body.append('\n%s\n' % self.entry_sep) + self.entry_sep = '@tab' + + def depart_entry(self, node: Element) -> None: + for _i in range(node.get('morecols', 0)): + self.body.append('\n@tab\n') + + # -- Field Lists + + def visit_field_list(self, node: Element) -> None: + pass + + def depart_field_list(self, node: Element) -> None: + pass + + def visit_field(self, node: Element) -> None: + self.body.append('\n') + + def depart_field(self, node: Element) -> None: + self.body.append('\n') + + def visit_field_name(self, node: Element) -> None: + self.ensure_eol() + self.body.append('@*') + + def depart_field_name(self, node: Element) -> None: + self.body.append(': ') + + def visit_field_body(self, node: Element) -> None: + pass + + def depart_field_body(self, node: Element) -> None: + pass + + # -- Admonitions + + def visit_admonition(self, node: Element, name: str = '') -> None: + if not name: + title = cast(nodes.title, node[0]) + name = self.escape(title.astext()) + self.body.append('\n@cartouche\n@quotation %s ' % name) + + def _visit_named_admonition(self, node: Element) -> None: + label = admonitionlabels[node.tagname] + self.body.append('\n@cartouche\n@quotation %s ' % label) + + def depart_admonition(self, node: Element) -> None: + self.ensure_eol() + self.body.append('@end quotation\n' + '@end cartouche\n') + + visit_attention = _visit_named_admonition + depart_attention = depart_admonition + visit_caution = _visit_named_admonition + depart_caution = depart_admonition + visit_danger = _visit_named_admonition + depart_danger = depart_admonition + visit_error = _visit_named_admonition + depart_error = depart_admonition + visit_hint = _visit_named_admonition + depart_hint = depart_admonition + visit_important = _visit_named_admonition + depart_important = depart_admonition + visit_note = _visit_named_admonition + depart_note = depart_admonition + visit_tip = _visit_named_admonition + depart_tip = depart_admonition + visit_warning = _visit_named_admonition + depart_warning = depart_admonition + + # -- Misc + + def visit_docinfo(self, node: Element) -> None: + raise nodes.SkipNode + + def visit_generated(self, node: Element) -> None: + raise nodes.SkipNode + + def visit_header(self, node: Element) -> None: + raise nodes.SkipNode + + def visit_footer(self, node: Element) -> None: + raise nodes.SkipNode + + def visit_container(self, node: Element) -> None: + if node.get('literal_block'): + self.body.append('\n\n@float LiteralBlock\n') + + def depart_container(self, node: Element) -> None: + if node.get('literal_block'): + self.body.append('\n@end float\n\n') + + def visit_decoration(self, node: Element) -> None: + pass + + def depart_decoration(self, node: Element) -> None: + pass + + def visit_topic(self, node: Element) -> None: + # ignore TOC's since we have to have a "menu" anyway + if 'contents' in node.get('classes', []): + raise nodes.SkipNode + title = cast(nodes.title, node[0]) + self.visit_rubric(title) + self.body.append('%s\n' % self.escape(title.astext())) + self.depart_rubric(title) + + def depart_topic(self, node: Element) -> None: + pass + + def visit_transition(self, node: Element) -> None: + self.body.append('\n\n%s\n\n' % ('_' * 66)) + + def depart_transition(self, node: Element) -> None: + pass + + def visit_attribution(self, node: Element) -> None: + self.body.append('\n\n@center --- ') + + def depart_attribution(self, node: Element) -> None: + self.body.append('\n\n') + + def visit_raw(self, node: Element) -> None: + format = node.get('format', '').split() + if 'texinfo' in format or 'texi' in format: + self.body.append(node.astext()) + raise nodes.SkipNode + + def visit_figure(self, node: Element) -> None: + self.body.append('\n\n@float Figure\n') + + def depart_figure(self, node: Element) -> None: + self.body.append('\n@end float\n\n') + + def visit_caption(self, node: Element) -> None: + if (isinstance(node.parent, nodes.figure) or + (isinstance(node.parent, nodes.container) and + node.parent.get('literal_block'))): + self.body.append('\n@caption{') + else: + logger.warning(__('caption not inside a figure.'), + location=node) + + def depart_caption(self, node: Element) -> None: + if (isinstance(node.parent, nodes.figure) or + (isinstance(node.parent, nodes.container) and + node.parent.get('literal_block'))): + self.body.append('}\n') + + def visit_image(self, node: Element) -> None: + if node['uri'] in self.builder.images: + uri = self.builder.images[node['uri']] + else: + # missing image! + if self.ignore_missing_images: + return + uri = node['uri'] + if uri.find('://') != -1: + # ignore remote images + return + name, ext = path.splitext(uri) + # width and height ignored in non-tex output + width = self.tex_image_length(node.get('width', '')) + height = self.tex_image_length(node.get('height', '')) + alt = self.escape_arg(node.get('alt', '')) + filename = f"{self.elements['filename'][:-5]}-figures/{name}" # type: ignore[index] + self.body.append('\n@image{%s,%s,%s,%s,%s}\n' % + (filename, width, height, alt, ext[1:])) + + def depart_image(self, node: Element) -> None: + pass + + def visit_compound(self, node: Element) -> None: + pass + + def depart_compound(self, node: Element) -> None: + pass + + def visit_sidebar(self, node: Element) -> None: + self.visit_topic(node) + + def depart_sidebar(self, node: Element) -> None: + self.depart_topic(node) + + def visit_label(self, node: Element) -> None: + # label numbering is automatically generated by Texinfo + if self.in_footnote: + raise nodes.SkipNode + self.body.append('@w{(') + + def depart_label(self, node: Element) -> None: + self.body.append(')} ') + + def visit_legend(self, node: Element) -> None: + pass + + def depart_legend(self, node: Element) -> None: + pass + + def visit_substitution_reference(self, node: Element) -> None: + pass + + def depart_substitution_reference(self, node: Element) -> None: + pass + + def visit_substitution_definition(self, node: Element) -> None: + raise nodes.SkipNode + + def visit_system_message(self, node: Element) -> None: + self.body.append('\n@verbatim\n' + '\n' + '@end verbatim\n' % node.astext()) + raise nodes.SkipNode + + def visit_comment(self, node: Element) -> None: + self.body.append('\n') + for line in node.astext().splitlines(): + self.body.append('@c %s\n' % line) + raise nodes.SkipNode + + def visit_problematic(self, node: Element) -> None: + self.body.append('>>') + + def depart_problematic(self, node: Element) -> None: + self.body.append('<<') + + def unimplemented_visit(self, node: Element) -> None: + logger.warning(__("unimplemented node type: %r"), node, + location=node) + + def unknown_departure(self, node: Node) -> None: + pass + + # -- Sphinx specific + + def visit_productionlist(self, node: Element) -> None: + self.visit_literal_block(None) + names = [] + productionlist = cast(Iterable[addnodes.production], node) + for production in productionlist: + names.append(production['tokenname']) + maxlen = max(len(name) for name in names) + for production in productionlist: + if production['tokenname']: + for id in production.get('ids'): + self.add_anchor(id, production) + s = production['tokenname'].ljust(maxlen) + ' ::=' + else: + s = '%s ' % (' ' * maxlen) + self.body.append(self.escape(s)) + self.body.append(self.escape(production.astext() + '\n')) + self.depart_literal_block(None) + raise nodes.SkipNode + + def visit_production(self, node: Element) -> None: + pass + + def depart_production(self, node: Element) -> None: + pass + + def visit_literal_emphasis(self, node: Element) -> None: + self.body.append('@code{') + + def depart_literal_emphasis(self, node: Element) -> None: + self.body.append('}') + + def visit_literal_strong(self, node: Element) -> None: + self.body.append('@code{') + + def depart_literal_strong(self, node: Element) -> None: + self.body.append('}') + + def visit_index(self, node: Element) -> None: + # terminate the line but don't prevent paragraph breaks + if isinstance(node.parent, nodes.paragraph): + self.ensure_eol() + else: + self.body.append('\n') + for (_entry_type, value, _target_id, _main, _category_key) in node['entries']: + text = self.escape_menu(value) + self.body.append('@geindex %s\n' % text) + + def visit_versionmodified(self, node: Element) -> None: + self.body.append('\n') + + def depart_versionmodified(self, node: Element) -> None: + self.body.append('\n') + + def visit_start_of_file(self, node: Element) -> None: + # add a document target + self.next_section_ids.add(':doc') + self.curfilestack.append(node['docname']) + self.footnotestack.append(self.collect_footnotes(node)) + + def depart_start_of_file(self, node: Element) -> None: + self.curfilestack.pop() + self.footnotestack.pop() + + def visit_centered(self, node: Element) -> None: + txt = self.escape_arg(node.astext()) + self.body.append('\n\n@center %s\n\n' % txt) + raise nodes.SkipNode + + def visit_seealso(self, node: Element) -> None: + self.body.append('\n\n@subsubheading %s\n\n' % + admonitionlabels['seealso']) + + def depart_seealso(self, node: Element) -> None: + self.body.append('\n') + + def visit_meta(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.body.append('\n\n') + self.body.append(', '.join(n.astext() for n in list_items) + '.') + self.body.append('\n\n') + raise nodes.SkipNode + + ############################################################# + # Domain-specific object descriptions + ############################################################# + + # Top-level nodes for descriptions + ################################## + + def visit_desc(self, node: addnodes.desc) -> None: + self.descs.append(node) + self.at_deffnx = '@deffn' + + def depart_desc(self, node: addnodes.desc) -> None: + self.descs.pop() + self.ensure_eol() + self.body.append('@end deffn\n') + + def visit_desc_signature(self, node: Element) -> None: + self.escape_hyphens += 1 + objtype = node.parent['objtype'] + if objtype != 'describe': + for id in node.get('ids'): + self.add_anchor(id, node) + # use the full name of the objtype for the category + try: + domain = self.builder.env.get_domain(node.parent['domain']) + name = domain.get_type_name(domain.object_types[objtype], + self.config.primary_domain == domain.name) + except (KeyError, ExtensionError): + name = objtype + # by convention, the deffn category should be capitalized like a title + category = self.escape_arg(smart_capwords(name)) + self.body.append(f'\n{self.at_deffnx} {{{category}}} ') + self.at_deffnx = '@deffnx' + self.desc_type_name: str | None = name + + def depart_desc_signature(self, node: Element) -> None: + self.body.append("\n") + self.escape_hyphens -= 1 + self.desc_type_name = None + + def visit_desc_signature_line(self, node: Element) -> None: + pass + + def depart_desc_signature_line(self, node: Element) -> None: + pass + + def visit_desc_content(self, node: Element) -> None: + pass + + def depart_desc_content(self, node: Element) -> None: + pass + + 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 + text = self.escape(node.astext()) + # replace no-break spaces with normal ones + text = text.replace(' ', '@w{ }') + self.body.append(text) + raise nodes.SkipNode + + def visit_desc_type_parameter(self, node: Element) -> None: + self.visit_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: + # Try to avoid duplicating info already displayed by the deffn category. + # e.g. + # @deffn {Class} Foo + # -- instead of -- + # @deffn {Class} class Foo + txt = node.astext().strip() + if ((self.descs and txt == self.descs[-1]['objtype']) or + (self.desc_type_name and txt in self.desc_type_name.split())): + raise nodes.SkipNode + + def depart_desc_annotation(self, node: Element) -> None: + pass + + ############################################## + + def visit_inline(self, node: Element) -> None: + pass + + def depart_inline(self, node: Element) -> None: + pass + + def visit_abbreviation(self, node: Element) -> None: + abbr = node.astext() + self.body.append('@abbr{') + if node.hasattr('explanation') and abbr not in self.handled_abbrs: + self.context.append(',%s}' % self.escape_arg(node['explanation'])) + self.handled_abbrs.add(abbr) + else: + self.context.append('}') + + def depart_abbreviation(self, node: Element) -> None: + self.body.append(self.context.pop()) + + def visit_manpage(self, node: Element) -> None: + return self.visit_literal_emphasis(node) + + def depart_manpage(self, node: Element) -> None: + return self.depart_literal_emphasis(node) + + def visit_download_reference(self, node: Element) -> None: + pass + + def depart_download_reference(self, node: Element) -> None: + pass + + 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_pending_xref(self, node: Element) -> None: + pass + + def depart_pending_xref(self, node: Element) -> None: + pass + + def visit_math(self, node: Element) -> None: + self.body.append('@math{' + self.escape_arg(node.astext()) + '}') + raise nodes.SkipNode + + def visit_math_block(self, node: Element) -> None: + if node.get('label'): + self.add_anchor(node['label'], node) + self.body.append('\n\n@example\n%s\n@end example\n\n' % + self.escape_arg(node.astext())) + raise nodes.SkipNode -- cgit v1.2.3