| """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 |