summaryrefslogtreecommitdiffstats
path: root/sphinx/writers/texinfo.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/writers/texinfo.py')
-rw-r--r--sphinx/writers/texinfo.py1572
1 files changed, 1572 insertions, 0 deletions
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 '<untitled>'
+ elements['title'] = self.escape_id(title) or '<untitled>'
+ # 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 '<untitled>'
+ 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'
+ '<SYSTEM MESSAGE: %s>\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