From cf7da1843c45a4c2df7a749f7886a2d2ba0ee92a Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Mon, 15 Apr 2024 19:25:40 +0200 Subject: Adding upstream version 7.2.6. Signed-off-by: Daniel Baumann --- sphinx/writers/text.py | 1305 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1305 insertions(+) create mode 100644 sphinx/writers/text.py (limited to 'sphinx/writers/text.py') diff --git a/sphinx/writers/text.py b/sphinx/writers/text.py new file mode 100644 index 0000000..63df2d7 --- /dev/null +++ b/sphinx/writers/text.py @@ -0,0 +1,1305 @@ +"""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() -- cgit v1.2.3