summaryrefslogtreecommitdiffstats
path: root/sphinx/writers/text.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/writers/text.py')
-rw-r--r--sphinx/writers/text.py1305
1 files changed, 1305 insertions, 0 deletions
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"<Cell {self.text!r} {self.row}v{self.rowspan}/{self.col}>{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('<SYSTEM MESSAGE: %s>' % 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()