Source code for linuxdoc.rstFlatTable

# SPDX-License-Identifier: AGPL-3.0-or-later
#
# pylint: disable=missing-docstring, arguments-differ, invalid-name
# pylint: disable=too-many-arguments, too-many-locals, too-many-branches
# pylint: disable=too-many-nested-blocks, useless-object-inheritance

"""\
flat-table
~~~~~~~~~~

Implementation of the ``flat-table`` reST-directive.  User documentation see
:ref:`rest-flat-table`

"""

# ==============================================================================
# imports
# ==============================================================================

from docutils import nodes
from docutils.parsers.rst import directives, roles
from docutils.parsers.rst.directives.tables import Table, align
from docutils.utils import SystemMessagePropagation

# ==============================================================================
# common globals
# ==============================================================================

__version__ = "3.0"


[docs] def setup(app): app.add_directive("flat-table", FlatTable) roles.register_local_role("cspan", c_span) roles.register_local_role("rspan", r_span) return dict(version=__version__, parallel_read_safe=True, parallel_write_safe=True)
[docs] def c_span( # pylint: disable=unused-argument name, rawtext, text, lineno, inliner, options=None, content=None ): options = options if options is not None else {} content = content if content is not None else [] nodelist = [colSpan(span=int(text))] msglist = [] return nodelist, msglist
[docs] def r_span( # pylint: disable=unused-argument name, rawtext, text, lineno, inliner, options=None, content=None ): options = options if options is not None else {} content = content if content is not None else [] nodelist = [rowSpan(span=int(text))] msglist = [] return nodelist, msglist
[docs] class rowSpan(nodes.General, nodes.Element): pass
[docs] class colSpan(nodes.General, nodes.Element): pass
[docs] class FlatTable(Table): """FlatTable (``flat-table``) directive""" option_spec = { "name": directives.unchanged, "class": directives.class_option, "header-rows": directives.nonnegative_int, "stub-columns": directives.nonnegative_int, "width": directives.length_or_percentage_or_unitless, "widths": directives.value_or(("auto", "grid"), directives.positive_int_list), "fill-cells": directives.flag, "align": align, }
[docs] def run(self): if not self.content: error = self.state_machine.reporter.error( 'The "%s" directive is empty; content required.' % self.name, nodes.literal_block(self.block_text, self.block_text), line=self.lineno, ) return [error] title, messages = self.make_title() node = nodes.Element() # anonymous container for parsing self.state.nested_parse(self.content, self.content_offset, node) tableBuilder = ListTableBuilder(self) tableBuilder.parseFlatTableNode(node) tableNode = tableBuilder.buildTableNode() self.add_name(tableNode) tableNode["classes"] += self.options.get("class", []) self.set_table_width(tableNode) if "align" in self.options: tableNode["align"] = self.options.get("align") # debug --> tableNode.asdom().toprettyxml() if title: tableNode.insert(0, title) return [tableNode] + messages
[docs] class ListTableBuilder(object): """Builds a table from a double-stage list""" def __init__(self, directive): self.directive = directive self.rows = [] self.max_cols = 0
[docs] def buildTableNode(self): colwidths = self.directive.get_column_widths(self.max_cols) stub_columns = self.directive.options.get("stub-columns", 0) header_rows = self.directive.options.get("header-rows", 0) table = nodes.table() if self.directive.widths == "auto": table["classes"] += ["colwidths-auto"] elif self.directive.widths: # explicitly set column widths table["classes"] += ["colwidths-given"] tgroup = nodes.tgroup(cols=len(colwidths)) table += tgroup for colwidth in colwidths: colspec = nodes.colspec(colwidth=colwidth) # ToDo: It seems, that the stub method only works well in the # absence of rowspan (observed by the html buidler, the docutils-xml # build seems OK). This is not extraordinary, because there exists # no table directive (except *this* flat-table) which allows to # define coexistent of rowspan and stubs (there was no use-case # before flat-table). This should be reviewed (later). if stub_columns: colspec.attributes["stub"] = 1 stub_columns -= 1 tgroup += colspec if header_rows: thead = nodes.thead() tgroup += thead for row in self.rows[:header_rows]: thead += self.buildTableRowNode(row) tbody = nodes.tbody() tgroup += tbody for row in self.rows[header_rows:]: tbody += self.buildTableRowNode(row) return table
[docs] def buildTableRowNode(self, row_data, classes=None): classes = [] if classes is None else classes row = nodes.row() for cell in row_data: if cell is None: continue cspan, rspan, cellElements = cell attributes = {"classes": classes} if rspan: attributes["morerows"] = rspan if cspan: attributes["morecols"] = cspan entry = nodes.entry(**attributes) entry.extend(cellElements) row += entry return row
[docs] def raiseError(self, msg): error = self.directive.state_machine.reporter.error( msg, nodes.literal_block(self.directive.block_text, self.directive.block_text), line=self.directive.lineno, ) raise SystemMessagePropagation(error)
[docs] def parseFlatTableNode(self, node): """parses the node from a :py:class:`FlatTable` directive's body""" if len(node) != 1 or not isinstance(node[0], nodes.bullet_list): self.raiseError( 'Error parsing content block for the "%s" directive: ' "exactly one bullet list expected." % self.directive.name ) for rowNum, rowItem in enumerate(node[0]): row = self.parseRowItem(rowItem, rowNum) self.rows.append(row) self.roundOffTableDefinition()
[docs] def roundOffTableDefinition(self): """Round off the table definition. This method rounds off the table definition in :py:attr:`rows`. * This method inserts the needed ``None`` values for the missing cells arising from spanning cells over rows and/or columns. * recount the :py:attr:`max_cols` * Autospan or fill (option ``fill-cells``) missing cells on the right side of the table-row """ y = 0 while y < len(self.rows): x = 0 while x < len(self.rows[y]): cell = self.rows[y][x] if cell is None: x += 1 continue cspan, rspan = cell[:2] # handle colspan in current row for c in range(cspan): try: self.rows[y].insert(x + c + 1, None) except Exception: # pylint: disable=broad-except # the user sets ambiguous rowspans pass # handle colspan in spanned rows for r in range(rspan): for c in range(cspan + 1): try: self.rows[y + r + 1].insert(x + c, None) except Exception: # pylint: disable=broad-except # the user sets ambiguous rowspans pass x += 1 y += 1 # Insert the missing cells on the right side. For this, first # re-calculate the max columns. for row in self.rows: if self.max_cols < len(row): # pylint: disable=consider-using-max-builtin self.max_cols = len(row) # fill with empty cells or cellspan? fill_cells = False if "fill-cells" in self.directive.options: fill_cells = True for row in self.rows: x = self.max_cols - len(row) if x and not fill_cells: if row[-1] is None: row.append((x - 1, 0, [])) else: cspan, rspan, content = row[-1] row[-1] = (cspan + x, rspan, content) elif x and fill_cells: for _i in range(x): row.append((0, 0, nodes.comment()))
[docs] def pprint(self): # for debugging retVal = "[ " for row in self.rows: retVal += "[ " for col in row: if col is None: retVal += "%r" % col retVal += "\n , " else: content = col[2][0].astext() if len(content) > 30: content = content[:30] + "..." retVal += "(cspan=%s, rspan=%s, %r)" % (col[0], col[1], content) retVal += "]\n , " retVal = retVal[:-2] retVal += "]\n , " retVal = retVal[:-2] return retVal + "]"
[docs] def parseRowItem(self, rowItem, rowNum): row = [] childNo = 0 error = False cell = None target = None for child in rowItem: if isinstance(child, (nodes.comment, nodes.system_message)): pass elif isinstance(child, nodes.target): target = child elif isinstance(child, nodes.bullet_list): childNo += 1 cell = child else: error = True break if childNo != 1 or error: self.raiseError( 'Error parsing content block for the "%s" directive: ' "two-level bullet list expected, but row %s does not " "contain a second-level bullet list." % (self.directive.name, rowNum + 1) ) for cellItem in cell: cspan, rspan, cellElements = self.parseCellItem(cellItem) if target is not None: cellElements.insert(0, target) row.append((cspan, rspan, cellElements)) return row
[docs] def parseCellItem(self, cellItem): # search and remove cspan, rspan colspec from the first element in # this listItem (field). cspan = rspan = 0 if not len(cellItem): # pylint: disable=len-as-condition return cspan, rspan, [] for elem in cellItem[0]: if isinstance(elem, colSpan): cspan = elem.get("span") elem.parent.remove(elem) continue if isinstance(elem, rowSpan): rspan = elem.get("span") elem.parent.remove(elem) continue return cspan, rspan, cellItem[:]