1573 lines
52 KiB
Python
1573 lines
52 KiB
Python
"""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
|