"""Custom docutils writer for plain text.""" from __future__ import annotations import math import os import re import textwrap from collections.abc import Generator, Iterable, Sequence from itertools import chain, groupby from typing import TYPE_CHECKING, Any, cast from docutils import nodes, writers from docutils.utils import column_width from sphinx import addnodes from sphinx.locale import _, admonitionlabels from sphinx.util.docutils import SphinxTranslator if TYPE_CHECKING: from docutils.nodes import Element, Text from sphinx.builders.text import TextBuilder class Cell: """Represents a cell in a table. It can span multiple columns or multiple lines. """ def __init__(self, text: str = "", rowspan: int = 1, colspan: int = 1) -> None: self.text = text self.wrapped: list[str] = [] self.rowspan = rowspan self.colspan = colspan self.col: int | None = None self.row: int | None = None def __repr__(self) -> str: return f"{self.colspan}>" def __hash__(self) -> int: return hash((self.col, self.row)) def __bool__(self) -> bool: return self.text != '' and self.col is not None and self.row is not None def wrap(self, width: int) -> None: self.wrapped = my_wrap(self.text, width) class Table: """Represents a table, handling cells that can span multiple lines or rows, like:: +-----------+-----+ | AAA | BBB | +-----+-----+ | | | XXX | | | +-----+-----+ | DDD | CCC | +-----+-----------+ This class can be used in two ways, either: - With absolute positions: call ``table[line, col] = Cell(...)``, this overwrites any existing cell(s) at these positions. - With relative positions: call the ``add_row()`` and ``add_cell(Cell(...))`` as needed. Cells spanning multiple rows or multiple columns (having a colspan or rowspan greater than one) are automatically referenced by all the table cells they cover. This is a useful representation as we can simply check ``if self[x, y] is self[x, y+1]`` to recognize a rowspan. Colwidth is not automatically computed, it has to be given, either at construction time, or during the table construction. Example usage:: table = Table([6, 6]) table.add_cell(Cell("foo")) table.add_cell(Cell("bar")) table.set_separator() table.add_row() table.add_cell(Cell("FOO")) table.add_cell(Cell("BAR")) print(table) +--------+--------+ | foo | bar | |========|========| | FOO | BAR | +--------+--------+ """ def __init__(self, colwidth: list[int] | None = None) -> None: self.lines: list[list[Cell]] = [] self.separator = 0 self.colwidth: list[int] = (colwidth if colwidth is not None else []) self.current_line = 0 self.current_col = 0 def add_row(self) -> None: """Add a row to the table, to use with ``add_cell()``. It is not needed to call ``add_row()`` before the first ``add_cell()``. """ self.current_line += 1 self.current_col = 0 def set_separator(self) -> None: """Sets the separator below the current line.""" self.separator = len(self.lines) def add_cell(self, cell: Cell) -> None: """Add a cell to the current line, to use with ``add_row()``. To add a cell spanning multiple lines or rows, simply set the ``cell.colspan`` or ``cell.rowspan`` BEFORE inserting it into the table. """ while self[self.current_line, self.current_col]: self.current_col += 1 self[self.current_line, self.current_col] = cell self.current_col += cell.colspan def __getitem__(self, pos: tuple[int, int]) -> Cell: line, col = pos self._ensure_has_line(line + 1) self._ensure_has_column(col + 1) return self.lines[line][col] def __setitem__(self, pos: tuple[int, int], cell: Cell) -> None: line, col = pos self._ensure_has_line(line + cell.rowspan) self._ensure_has_column(col + cell.colspan) for dline in range(cell.rowspan): for dcol in range(cell.colspan): self.lines[line + dline][col + dcol] = cell cell.row = line cell.col = col def _ensure_has_line(self, line: int) -> None: while len(self.lines) < line: self.lines.append([]) def _ensure_has_column(self, col: int) -> None: for line in self.lines: while len(line) < col: line.append(Cell()) def __repr__(self) -> str: return "\n".join(repr(line) for line in self.lines) def cell_width(self, cell: Cell, source: list[int]) -> int: """Give the cell width, according to the given source (either ``self.colwidth`` or ``self.measured_widths``). This takes into account cells spanning multiple columns. """ if cell.row is None or cell.col is None: msg = 'Cell co-ordinates have not been set' raise ValueError(msg) width = 0 for i in range(self[cell.row, cell.col].colspan): width += source[cell.col + i] return width + (cell.colspan - 1) * 3 @property def cells(self) -> Generator[Cell, None, None]: seen: set[Cell] = set() for line in self.lines: for cell in line: if cell and cell not in seen: yield cell seen.add(cell) def rewrap(self) -> None: """Call ``cell.wrap()`` on all cells, and measure each column width after wrapping (result written in ``self.measured_widths``). """ self.measured_widths = self.colwidth[:] for cell in self.cells: cell.wrap(width=self.cell_width(cell, self.colwidth)) if not cell.wrapped: continue if cell.row is None or cell.col is None: msg = 'Cell co-ordinates have not been set' raise ValueError(msg) width = math.ceil(max(column_width(x) for x in cell.wrapped) / cell.colspan) for col in range(cell.col, cell.col + cell.colspan): self.measured_widths[col] = max(self.measured_widths[col], width) def physical_lines_for_line(self, line: list[Cell]) -> int: """For a given line, compute the number of physical lines it spans due to text wrapping. """ physical_lines = 1 for cell in line: physical_lines = max(physical_lines, len(cell.wrapped)) return physical_lines def __str__(self) -> str: out = [] self.rewrap() def writesep(char: str = "-", lineno: int | None = None) -> str: """Called on the line *before* lineno. Called with no *lineno* for the last sep. """ out: list[str] = [] for colno, width in enumerate(self.measured_widths): if ( lineno is not None and lineno > 0 and self[lineno, colno] is self[lineno - 1, colno] ): out.append(" " * (width + 2)) else: out.append(char * (width + 2)) head = "+" if out[0][0] == "-" else "|" tail = "+" if out[-1][0] == "-" else "|" glue = [ "+" if left[0] == "-" or right[0] == "-" else "|" for left, right in zip(out, out[1:]) ] glue.append(tail) return head + "".join(chain.from_iterable(zip(out, glue))) for lineno, line in enumerate(self.lines): if self.separator and lineno == self.separator: out.append(writesep("=", lineno)) else: out.append(writesep("-", lineno)) for physical_line in range(self.physical_lines_for_line(line)): linestr = ["|"] for colno, cell in enumerate(line): if cell.col != colno: continue if lineno != cell.row: # NoQA: SIM114 physical_text = "" elif physical_line >= len(cell.wrapped): physical_text = "" else: physical_text = cell.wrapped[physical_line] adjust_len = len(physical_text) - column_width(physical_text) linestr.append( " " + physical_text.ljust( self.cell_width(cell, self.measured_widths) + 1 + adjust_len, ) + "|", ) out.append("".join(linestr)) out.append(writesep("-")) return "\n".join(out) class TextWrapper(textwrap.TextWrapper): """Custom subclass that uses a different word separator regex.""" wordsep_re = re.compile( r'(\s+|' # any whitespace r'(?<=\s)(?::[a-z-]+:)?`\S+|' # interpreted text start r'[^\s\w]*\w+[a-zA-Z]-(?=\w+[a-zA-Z])|' # hyphenated words r'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w))') # em-dash def _wrap_chunks(self, chunks: list[str]) -> list[str]: """_wrap_chunks(chunks : [string]) -> [string] The original _wrap_chunks uses len() to calculate width. This method respects wide/fullwidth characters for width adjustment. """ lines: list[str] = [] if self.width <= 0: raise ValueError("invalid width %r (must be > 0)" % self.width) chunks.reverse() while chunks: cur_line = [] cur_len = 0 if lines: indent = self.subsequent_indent else: indent = self.initial_indent width = self.width - column_width(indent) if self.drop_whitespace and chunks[-1].strip() == '' and lines: del chunks[-1] while chunks: l = column_width(chunks[-1]) if cur_len + l <= width: cur_line.append(chunks.pop()) cur_len += l else: break if chunks and column_width(chunks[-1]) > width: self._handle_long_word(chunks, cur_line, cur_len, width) if self.drop_whitespace and cur_line and cur_line[-1].strip() == '': del cur_line[-1] if cur_line: lines.append(indent + ''.join(cur_line)) return lines def _break_word(self, word: str, space_left: int) -> tuple[str, str]: """_break_word(word : string, space_left : int) -> (string, string) Break line by unicode width instead of len(word). """ total = 0 for i, c in enumerate(word): total += column_width(c) if total > space_left: return word[:i - 1], word[i - 1:] return word, '' def _split(self, text: str) -> list[str]: """_split(text : string) -> [string] Override original method that only split by 'wordsep_re'. This '_split' splits wide-characters into chunks by one character. """ def split(t: str) -> list[str]: return super(TextWrapper, self)._split(t) chunks: list[str] = [] for chunk in split(text): for w, g in groupby(chunk, column_width): if w == 1: chunks.extend(split(''.join(g))) else: chunks.extend(list(g)) return chunks def _handle_long_word(self, reversed_chunks: list[str], cur_line: list[str], cur_len: int, width: int) -> None: """_handle_long_word(chunks : [string], cur_line : [string], cur_len : int, width : int) Override original method for using self._break_word() instead of slice. """ space_left = max(width - cur_len, 1) if self.break_long_words: l, r = self._break_word(reversed_chunks[-1], space_left) cur_line.append(l) reversed_chunks[-1] = r elif not cur_line: cur_line.append(reversed_chunks.pop()) MAXWIDTH = 70 STDINDENT = 3 def my_wrap(text: str, width: int = MAXWIDTH, **kwargs: Any) -> list[str]: w = TextWrapper(width=width, **kwargs) return w.wrap(text) class TextWriter(writers.Writer): supported = ('text',) settings_spec = ('No options here.', '', ()) settings_defaults: dict[str, Any] = {} output: str def __init__(self, builder: TextBuilder) -> None: super().__init__() self.builder = builder def translate(self) -> None: visitor = self.builder.create_translator(self.document, self.builder) self.document.walkabout(visitor) self.output = cast(TextTranslator, visitor).body class TextTranslator(SphinxTranslator): builder: TextBuilder def __init__(self, document: nodes.document, builder: TextBuilder) -> None: super().__init__(document, builder) newlines = self.config.text_newlines if newlines == 'windows': self.nl = '\r\n' elif newlines == 'native': self.nl = os.linesep else: self.nl = '\n' self.sectionchars = self.config.text_sectionchars self.add_secnumbers = self.config.text_add_secnumbers self.secnumber_suffix = self.config.text_secnumber_suffix self.states: list[list[tuple[int, str | list[str]]]] = [[]] self.stateindent = [0] self.list_counter: list[int] = [] self.sectionlevel = 0 self.lineblocklevel = 0 self.table: Table self.context: list[str] = [] """Heterogeneous stack. Used by visit_* and depart_* functions in conjunction with the tree traversal. Make sure that the pops correspond to the pushes. """ def add_text(self, text: str) -> None: self.states[-1].append((-1, text)) def new_state(self, indent: int = STDINDENT) -> None: self.states.append([]) self.stateindent.append(indent) def end_state( self, wrap: bool = True, end: Sequence[str] | None = ('',), first: str | None = None, ) -> None: content = self.states.pop() maxindent = sum(self.stateindent) indent = self.stateindent.pop() result: list[tuple[int, list[str]]] = [] toformat: list[str] = [] def do_format() -> None: if not toformat: return if wrap: res = my_wrap(''.join(toformat), width=MAXWIDTH - maxindent) else: res = ''.join(toformat).splitlines() if end: res += end result.append((indent, res)) for itemindent, item in content: if itemindent == -1: toformat.append(item) # type: ignore[arg-type] else: do_format() result.append((indent + itemindent, item)) # type: ignore[arg-type] toformat = [] do_format() if first is not None and result: # insert prefix into first line (ex. *, [1], See also, etc.) newindent = result[0][0] - indent if result[0][1] == ['']: result.insert(0, (newindent, [first])) else: text = first + result[0][1].pop(0) result.insert(0, (newindent, [text])) self.states[-1].extend(result) def visit_document(self, node: Element) -> None: self.new_state(0) def depart_document(self, node: Element) -> None: self.end_state() self.body = self.nl.join(line and (' ' * indent + line) for indent, lines in self.states[0] for line in lines) # XXX header/footer? def visit_section(self, node: Element) -> None: self._title_char = self.sectionchars[self.sectionlevel] self.sectionlevel += 1 def depart_section(self, node: Element) -> None: self.sectionlevel -= 1 def visit_topic(self, node: Element) -> None: self.new_state(0) def depart_topic(self, node: Element) -> None: self.end_state() visit_sidebar = visit_topic depart_sidebar = depart_topic def visit_rubric(self, node: Element) -> None: self.new_state(0) self.add_text('-[ ') def depart_rubric(self, node: Element) -> None: self.add_text(' ]-') self.end_state() def visit_compound(self, node: Element) -> None: pass def depart_compound(self, node: Element) -> None: pass def visit_glossary(self, node: Element) -> None: pass def depart_glossary(self, node: Element) -> None: pass def visit_title(self, node: Element) -> None: if isinstance(node.parent, nodes.Admonition): self.add_text(node.astext() + ': ') raise nodes.SkipNode self.new_state(0) def get_section_number_string(self, node: Element) -> str: if isinstance(node.parent, nodes.section): anchorname = '#' + node.parent['ids'][0] numbers = self.builder.secnumbers.get(anchorname) if numbers is None: numbers = self.builder.secnumbers.get('') if numbers is not None: return '.'.join(map(str, numbers)) + self.secnumber_suffix return '' def depart_title(self, node: Element) -> None: if isinstance(node.parent, nodes.section): char = self._title_char else: char = '^' text = '' text = ''.join(x[1] for x in self.states.pop() if x[0] == -1) # type: ignore[misc] if self.add_secnumbers: text = self.get_section_number_string(node) + text self.stateindent.pop() title = ['', text, '%s' % (char * column_width(text)), ''] if len(self.states) == 2 and len(self.states[-1]) == 0: # remove an empty line before title if it is first section title in the document title.pop(0) self.states[-1].append((0, title)) def visit_subtitle(self, node: Element) -> None: pass def depart_subtitle(self, node: Element) -> None: pass def visit_attribution(self, node: Element) -> None: self.add_text('-- ') def depart_attribution(self, node: Element) -> None: pass ############################################################# # Domain-specific object descriptions ############################################################# # Top-level nodes ################# def visit_desc(self, node: Element) -> None: pass def depart_desc(self, node: Element) -> None: pass def visit_desc_signature(self, node: Element) -> None: self.new_state(0) def depart_desc_signature(self, node: Element) -> None: # XXX: wrap signatures in a way that makes sense self.end_state(wrap=False, end=None) def visit_desc_signature_line(self, node: Element) -> None: pass def depart_desc_signature_line(self, node: Element) -> None: self.add_text('\n') def visit_desc_content(self, node: Element) -> None: self.new_state() self.add_text(self.nl) def depart_desc_content(self, node: Element) -> None: self.end_state() 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.add_text(' -> ') def depart_desc_returns(self, node: Element) -> None: pass def _visit_sig_parameter_list( self, node: Element, parameter_group: type[Element], sig_open_paren: str, sig_close_paren: str, ) -> None: """Visit a signature parameters or type parameters list. The *parameter_group* value is the type of a child node acting as a required parameter or as a set of contiguous optional parameters. """ self.add_text(sig_open_paren) self.is_first_param = True self.optional_param_level = 0 self.params_left_at_level = 0 self.param_group_index = 0 # Counts as what we call a parameter group are either a required parameter, or a # set of contiguous optional ones. self.list_is_required_param = [isinstance(c, parameter_group) for c in node.children] self.required_params_left = sum(self.list_is_required_param) self.param_separator = ', ' self.multi_line_parameter_list = node.get('multi_line_parameter_list', False) if self.multi_line_parameter_list: self.param_separator = self.param_separator.rstrip() self.context.append(sig_close_paren) def _depart_sig_parameter_list(self, node: Element) -> None: sig_close_paren = self.context.pop() self.add_text(sig_close_paren) def visit_desc_parameterlist(self, node: Element) -> None: self._visit_sig_parameter_list(node, addnodes.desc_parameter, '(', ')') def depart_desc_parameterlist(self, node: Element) -> None: self._depart_sig_parameter_list(node) def visit_desc_type_parameter_list(self, node: Element) -> None: self._visit_sig_parameter_list(node, addnodes.desc_type_parameter, '[', ']') def depart_desc_type_parameter_list(self, node: Element) -> None: self._depart_sig_parameter_list(node) def visit_desc_parameter(self, node: Element) -> None: on_separate_line = self.multi_line_parameter_list if on_separate_line and not (self.is_first_param and self.optional_param_level > 0): self.new_state() if self.is_first_param: self.is_first_param = False elif not on_separate_line and not self.required_params_left: self.add_text(self.param_separator) if self.optional_param_level == 0: self.required_params_left -= 1 else: self.params_left_at_level -= 1 self.add_text(node.astext()) is_required = self.list_is_required_param[self.param_group_index] if on_separate_line: is_last_group = self.param_group_index + 1 == len(self.list_is_required_param) next_is_required = ( not is_last_group and self.list_is_required_param[self.param_group_index + 1] ) opt_param_left_at_level = self.params_left_at_level > 0 if opt_param_left_at_level or is_required and (is_last_group or next_is_required): self.add_text(self.param_separator) self.end_state(wrap=False, end=None) elif self.required_params_left: self.add_text(self.param_separator) if is_required: self.param_group_index += 1 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.params_left_at_level = sum([isinstance(c, addnodes.desc_parameter) for c in node.children]) self.optional_param_level += 1 self.max_optional_param_level = self.optional_param_level if self.multi_line_parameter_list: # If the first parameter is optional, start a new line and open the bracket. if self.is_first_param: self.new_state() self.add_text('[') # Else, if there remains at least one required parameter, append the # parameter separator, open a new bracket, and end the line. elif self.required_params_left: self.add_text(self.param_separator) self.add_text('[') self.end_state(wrap=False, end=None) # Else, open a new bracket, append the parameter separator, and end the # line. else: self.add_text('[') self.add_text(self.param_separator) self.end_state(wrap=False, end=None) else: self.add_text('[') def depart_desc_optional(self, node: Element) -> None: self.optional_param_level -= 1 if self.multi_line_parameter_list: # If it's the first time we go down one level, add the separator before the # bracket. if self.optional_param_level == self.max_optional_param_level - 1: self.add_text(self.param_separator) self.add_text(']') # End the line if we have just closed the last bracket of this group of # optional parameters. if self.optional_param_level == 0: self.end_state(wrap=False, end=None) else: self.add_text(']') if self.optional_param_level == 0: self.param_group_index += 1 def visit_desc_annotation(self, node: Element) -> None: pass def depart_desc_annotation(self, node: Element) -> None: pass ############################################## def visit_figure(self, node: Element) -> None: self.new_state() def depart_figure(self, node: Element) -> None: self.end_state() def visit_caption(self, node: Element) -> None: pass def depart_caption(self, node: Element) -> None: pass def visit_productionlist(self, node: Element) -> None: self.new_state() names = [] productionlist = cast(Iterable[addnodes.production], node) for production in productionlist: names.append(production['tokenname']) maxlen = max(len(name) for name in names) lastname = None for production in productionlist: if production['tokenname']: self.add_text(production['tokenname'].ljust(maxlen) + ' ::=') lastname = production['tokenname'] elif lastname is not None: self.add_text('%s ' % (' ' * len(lastname))) self.add_text(production.astext() + self.nl) self.end_state(wrap=False) raise nodes.SkipNode def visit_footnote(self, node: Element) -> None: label = cast(nodes.label, node[0]) self._footnote = label.astext().strip() self.new_state(len(self._footnote) + 3) def depart_footnote(self, node: Element) -> None: self.end_state(first='[%s] ' % self._footnote) def visit_citation(self, node: Element) -> None: if len(node) and isinstance(node[0], nodes.label): self._citlabel = node[0].astext() else: self._citlabel = '' self.new_state(len(self._citlabel) + 3) def depart_citation(self, node: Element) -> None: self.end_state(first='[%s] ' % self._citlabel) def visit_label(self, node: Element) -> None: raise nodes.SkipNode def visit_legend(self, node: Element) -> None: pass def depart_legend(self, node: Element) -> None: pass # XXX: option list could use some better styling def visit_option_list(self, node: Element) -> None: pass def depart_option_list(self, node: Element) -> None: pass def visit_option_list_item(self, node: Element) -> None: self.new_state(0) def depart_option_list_item(self, node: Element) -> None: self.end_state() def visit_option_group(self, node: Element) -> None: self._firstoption = True def depart_option_group(self, node: Element) -> None: self.add_text(' ') def visit_option(self, node: Element) -> None: if self._firstoption: self._firstoption = False else: self.add_text(', ') def depart_option(self, node: Element) -> None: pass 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.add_text(node['delimiter']) def depart_option_argument(self, node: Element) -> None: pass def visit_description(self, node: Element) -> None: pass def depart_description(self, node: Element) -> None: pass def visit_tabular_col_spec(self, node: Element) -> None: raise nodes.SkipNode def visit_colspec(self, node: Element) -> None: self.table.colwidth.append(node["colwidth"]) raise nodes.SkipNode def visit_tgroup(self, node: Element) -> None: pass def depart_tgroup(self, node: Element) -> None: pass def visit_thead(self, node: Element) -> None: pass def depart_thead(self, node: Element) -> None: pass def visit_tbody(self, node: Element) -> None: self.table.set_separator() def depart_tbody(self, node: Element) -> None: pass def visit_row(self, node: Element) -> None: if self.table.lines: self.table.add_row() def depart_row(self, node: Element) -> None: pass def visit_entry(self, node: Element) -> None: self.entry = Cell( rowspan=node.get("morerows", 0) + 1, colspan=node.get("morecols", 0) + 1, ) self.new_state(0) def depart_entry(self, node: Element) -> None: text = self.nl.join(self.nl.join(x[1]) for x in self.states.pop()) self.stateindent.pop() self.entry.text = text self.table.add_cell(self.entry) del self.entry def visit_table(self, node: Element) -> None: if hasattr(self, 'table'): msg = 'Nested tables are not supported.' raise NotImplementedError(msg) self.new_state(0) self.table = Table() def depart_table(self, node: Element) -> None: self.add_text(str(self.table)) del self.table self.end_state(wrap=False) 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.new_state(0) self.add_text(', '.join(n.astext() for n in list_items) + '.') self.end_state() raise nodes.SkipNode def visit_image(self, node: Element) -> None: if 'alt' in node.attributes: self.add_text(_('[image: %s]') % node['alt']) self.add_text(_('[image]')) raise nodes.SkipNode def visit_transition(self, node: Element) -> None: indent = sum(self.stateindent) self.new_state(0) self.add_text('=' * (MAXWIDTH - indent)) self.end_state() raise nodes.SkipNode def visit_bullet_list(self, node: Element) -> None: self.list_counter.append(-1) def depart_bullet_list(self, node: Element) -> None: self.list_counter.pop() def visit_enumerated_list(self, node: Element) -> None: self.list_counter.append(node.get('start', 1) - 1) def depart_enumerated_list(self, node: Element) -> None: self.list_counter.pop() def visit_definition_list(self, node: Element) -> None: self.list_counter.append(-2) def depart_definition_list(self, node: Element) -> None: self.list_counter.pop() def visit_list_item(self, node: Element) -> None: if self.list_counter[-1] == -1: # bullet list self.new_state(2) elif self.list_counter[-1] == -2: # definition list pass else: # enumerated list self.list_counter[-1] += 1 self.new_state(len(str(self.list_counter[-1])) + 2) def depart_list_item(self, node: Element) -> None: if self.list_counter[-1] == -1: self.end_state(first='* ') elif self.list_counter[-1] == -2: pass else: self.end_state(first='%s. ' % self.list_counter[-1]) def visit_definition_list_item(self, node: Element) -> None: self._classifier_count_in_li = len(list(node.findall(nodes.classifier))) def depart_definition_list_item(self, node: Element) -> None: pass def visit_term(self, node: Element) -> None: self.new_state(0) def depart_term(self, node: Element) -> None: if not self._classifier_count_in_li: self.end_state(end=None) def visit_classifier(self, node: Element) -> None: self.add_text(' : ') def depart_classifier(self, node: Element) -> None: self._classifier_count_in_li -= 1 if not self._classifier_count_in_li: self.end_state(end=None) def visit_definition(self, node: Element) -> None: self.new_state() def depart_definition(self, node: Element) -> None: self.end_state() 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: pass def depart_field(self, node: Element) -> None: pass def visit_field_name(self, node: Element) -> None: self.new_state(0) def depart_field_name(self, node: Element) -> None: self.add_text(':') self.end_state(end=None) def visit_field_body(self, node: Element) -> None: self.new_state() def depart_field_body(self, node: Element) -> None: self.end_state() def visit_centered(self, node: Element) -> None: pass def depart_centered(self, node: Element) -> None: pass def visit_hlist(self, node: Element) -> None: pass def depart_hlist(self, node: Element) -> None: pass def visit_hlistcol(self, node: Element) -> None: pass def depart_hlistcol(self, node: Element) -> None: pass def visit_admonition(self, node: Element) -> None: self.new_state(0) def depart_admonition(self, node: Element) -> None: self.end_state() def _visit_admonition(self, node: Element) -> None: self.new_state(2) def _depart_admonition(self, node: Element) -> None: label = admonitionlabels[node.tagname] indent = sum(self.stateindent) + len(label) if (len(self.states[-1]) == 1 and self.states[-1][0][0] == 0 and MAXWIDTH - indent >= sum(len(s) for s in self.states[-1][0][1])): # short text: append text after admonition label self.stateindent[-1] += len(label) self.end_state(first=label + ': ') else: # long text: append label before the block self.states[-1].insert(0, (0, [self.nl])) self.end_state(first=label + ':') visit_attention = _visit_admonition depart_attention = _depart_admonition visit_caution = _visit_admonition depart_caution = _depart_admonition visit_danger = _visit_admonition depart_danger = _depart_admonition visit_error = _visit_admonition depart_error = _depart_admonition visit_hint = _visit_admonition depart_hint = _depart_admonition visit_important = _visit_admonition depart_important = _depart_admonition visit_note = _visit_admonition depart_note = _depart_admonition visit_tip = _visit_admonition depart_tip = _depart_admonition visit_warning = _visit_admonition depart_warning = _depart_admonition visit_seealso = _visit_admonition depart_seealso = _depart_admonition def visit_versionmodified(self, node: Element) -> None: self.new_state(0) def depart_versionmodified(self, node: Element) -> None: self.end_state() def visit_literal_block(self, node: Element) -> None: self.new_state() def depart_literal_block(self, node: Element) -> None: self.end_state(wrap=False) def visit_doctest_block(self, node: Element) -> None: self.new_state(0) def depart_doctest_block(self, node: Element) -> None: self.end_state(wrap=False) def visit_line_block(self, node: Element) -> None: self.new_state() self.lineblocklevel += 1 def depart_line_block(self, node: Element) -> None: self.lineblocklevel -= 1 self.end_state(wrap=False, end=None) if not self.lineblocklevel: self.add_text('\n') def visit_line(self, node: Element) -> None: pass def depart_line(self, node: Element) -> None: self.add_text('\n') def visit_block_quote(self, node: Element) -> None: self.new_state() def depart_block_quote(self, node: Element) -> None: self.end_state() def visit_compact_paragraph(self, node: Element) -> None: pass def depart_compact_paragraph(self, node: Element) -> None: pass def visit_paragraph(self, node: Element) -> None: if not isinstance(node.parent, nodes.Admonition) or \ isinstance(node.parent, addnodes.seealso): self.new_state(0) def depart_paragraph(self, node: Element) -> None: if not isinstance(node.parent, nodes.Admonition) or \ isinstance(node.parent, addnodes.seealso): self.end_state() def visit_target(self, node: Element) -> None: raise nodes.SkipNode def visit_index(self, node: Element) -> None: raise nodes.SkipNode def visit_toctree(self, node: Element) -> None: raise nodes.SkipNode def visit_substitution_definition(self, node: Element) -> None: raise nodes.SkipNode def visit_pending_xref(self, node: Element) -> None: pass def depart_pending_xref(self, node: Element) -> None: pass def visit_reference(self, node: Element) -> None: if self.add_secnumbers: numbers = node.get("secnumber") if numbers is not None: self.add_text('.'.join(map(str, numbers)) + self.secnumber_suffix) 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_download_reference(self, node: Element) -> None: pass def depart_download_reference(self, node: Element) -> None: pass def visit_emphasis(self, node: Element) -> None: self.add_text('*') def depart_emphasis(self, node: Element) -> None: self.add_text('*') def visit_literal_emphasis(self, node: Element) -> None: self.add_text('*') def depart_literal_emphasis(self, node: Element) -> None: self.add_text('*') def visit_strong(self, node: Element) -> None: self.add_text('**') def depart_strong(self, node: Element) -> None: self.add_text('**') def visit_literal_strong(self, node: Element) -> None: self.add_text('**') def depart_literal_strong(self, node: Element) -> None: self.add_text('**') def visit_abbreviation(self, node: Element) -> None: self.add_text('') def depart_abbreviation(self, node: Element) -> None: if node.hasattr('explanation'): self.add_text(' (%s)' % node['explanation']) 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_title_reference(self, node: Element) -> None: self.add_text('*') def depart_title_reference(self, node: Element) -> None: self.add_text('*') def visit_literal(self, node: Element) -> None: self.add_text('"') def depart_literal(self, node: Element) -> None: self.add_text('"') def visit_subscript(self, node: Element) -> None: self.add_text('_') def depart_subscript(self, node: Element) -> None: pass def visit_superscript(self, node: Element) -> None: self.add_text('^') def depart_superscript(self, node: Element) -> None: pass def visit_footnote_reference(self, node: Element) -> None: self.add_text('[%s]' % node.astext()) raise nodes.SkipNode def visit_citation_reference(self, node: Element) -> None: self.add_text('[%s]' % node.astext()) raise nodes.SkipNode def visit_Text(self, node: Text) -> None: self.add_text(node.astext()) def depart_Text(self, node: Text) -> None: pass def visit_generated(self, node: Element) -> None: pass def depart_generated(self, node: Element) -> None: pass def visit_inline(self, node: Element) -> None: if 'xref' in node['classes'] or 'term' in node['classes']: self.add_text('*') def depart_inline(self, node: Element) -> None: if 'xref' in node['classes'] or 'term' in node['classes']: self.add_text('*') def visit_container(self, node: Element) -> None: pass def depart_container(self, node: Element) -> None: pass def visit_problematic(self, node: Element) -> None: self.add_text('>>') def depart_problematic(self, node: Element) -> None: self.add_text('<<') def visit_system_message(self, node: Element) -> None: self.new_state(0) self.add_text('' % node.astext()) self.end_state() raise nodes.SkipNode def visit_comment(self, node: Element) -> None: raise nodes.SkipNode def visit_meta(self, node: Element) -> None: # only valid for HTML raise nodes.SkipNode def visit_raw(self, node: Element) -> None: if 'text' in node.get('format', '').split(): self.new_state(0) self.add_text(node.astext()) self.end_state(wrap = False) raise nodes.SkipNode def visit_math(self, node: Element) -> None: pass def depart_math(self, node: Element) -> None: pass def visit_math_block(self, node: Element) -> None: self.new_state() def depart_math_block(self, node: Element) -> None: self.end_state()