"""Custom docutils writer for Texinfo."""
import re
import textwrap
import warnings
from os import path
from typing import (TYPE_CHECKING, Any, Dict, Iterable, Iterator, List, Optional, Pattern, Set,
Tuple, Union, cast)
from docutils import nodes, writers
from docutils.nodes import Element, Node, Text
from sphinx import __display_version__, addnodes
from sphinx.deprecation import RemovedInSphinx50Warning
from sphinx.domains import IndexEntry
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 sphinx.builders.texinfo import TexinfoBuilder
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
@definfoenclose strong,`,'
@definfoenclose emph,`,'
@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
elif isinstance(child, nodes.Element):
result.extend(find_subsections(child))
return result
def smart_capwords(s: str, sep: str = 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 = {}
output: str = None
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):
builder: "TexinfoBuilder" = None
ignore_missing_images = False
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
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[Union[collected_footnote, bool]]]] = [] # NOQA
self.in_footnote = 0
self.in_samp = 0
self.handled_abbrs: Set[str] = set()
self.colwidths: List[int] = None
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('@unnumbered %s\n\n%s\n' % (name, content))
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{%s}@w{%s}\n' % (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
elements['filename'] = elements['filename'][:-4] # type: ignore
elements['filename'] += '.info' # type: ignore
# 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']
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 = '* %s:: ' % (name,)
else:
s = '* %s: %s. ' % (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: Pattern = 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('\n%s\n\n' % (self.escape(self.node_names[name],)))
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('%s:%s' % (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 = '%s-%s' % (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[Union[collected_footnote, bool]]]: # NOQA
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[Union[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('@ref{%s,,%s}' % (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
elif 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('@email{%s,%s}' % (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('@ref{%s,,,%s}' % (id, uri))
else:
self.body.append('@ref{%s,,%s,%s}' % (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('@uref{%s,%s}' % (uri, name))
elif show_urls == 'no':
self.body.append('@uref{%s,,%s}' % (uri, name))
else:
self.body.append('%s@footnote{%s}' % (name, 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:
self.body.append('\n@example\n')
def depart_literal_block(self, node: Element) -> 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('@strong{')
def depart_strong(self, node: Element) -> None:
self.body.append('}')
def visit_emphasis(self, node: Element) -> None:
element = 'emph' if not self.in_samp else 'var'
self.body.append('@%s{' % element)
def depart_emphasis(self, node: Element) -> None:
self.body.append('}')
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
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 = "%s-figures/%s" % (self.elements['filename'][:-5], name) # type: ignore
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
else:
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 in node['entries']:
typ, text, tid, text2, key_ = entry
text = self.escape_menu(text)
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('\n%s {%s} ' % (self.at_deffnx, category))
self.at_deffnx = '@deffnx'
self.desc_type_name = 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_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_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
@property
def desc(self) -> Optional[addnodes.desc]:
warnings.warn('TexinfoWriter.desc is deprecated.', RemovedInSphinx50Warning)
if len(self.descs):
return self.descs[-1]
else:
return None