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): # ============================================================================== u"""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): # ============================================================================== u"""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) # FIXME: 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): u"""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): u"""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): 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[:]