| #!/usr/bin/env python3 |
| # -*- coding: utf-8; mode: python -*- |
| # pylint: disable=C0330, R0903, R0912 |
| |
| u""" |
| flat-table |
| ~~~~~~~~~~ |
| |
| Implementation of the ``flat-table`` reST-directive. |
| |
| :copyright: Copyright (C) 2016 Markus Heiser |
| :license: GPL Version 2, June 1991 see linux/COPYING for details. |
| |
| The ``flat-table`` (:py:class:`FlatTable`) is a double-stage list similar to |
| the ``list-table`` with some additional features: |
| |
| * *column-span*: with the role ``cspan`` a cell can be extended through |
| additional columns |
| |
| * *row-span*: with the role ``rspan`` a cell can be extended through |
| additional rows |
| |
| * *auto span* rightmost cell of a table row over the missing cells on the |
| right side of that table-row. With Option ``:fill-cells:`` this behavior |
| can changed from *auto span* to *auto fill*, which automaticly inserts |
| (empty) cells instead of spanning the last cell. |
| |
| Options: |
| |
| * header-rows: [int] count of header rows |
| * stub-columns: [int] count of stub columns |
| * widths: [[int] [int] ... ] widths of columns |
| * fill-cells: instead of autospann missing cells, insert missing cells |
| |
| roles: |
| |
| * cspan: [int] additionale columns (*morecols*) |
| * rspan: [int] additionale rows (*morerows*) |
| """ |
| |
| # ============================================================================== |
| # imports |
| # ============================================================================== |
| |
| from docutils import nodes |
| from docutils.parsers.rst import directives, roles |
| from docutils.parsers.rst.directives.tables import Table |
| from docutils.utils import SystemMessagePropagation |
| |
| # ============================================================================== |
| # common globals |
| # ============================================================================== |
| |
| __version__ = '1.0' |
| |
| # ============================================================================== |
| 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 |
| ) |
| |
| # ============================================================================== |
| def c_span(name, rawtext, text, lineno, inliner, options=None, content=None): |
| # ============================================================================== |
| # pylint: disable=W0613 |
| |
| 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 |
| |
| # ============================================================================== |
| def r_span(name, rawtext, text, lineno, inliner, options=None, content=None): |
| # ============================================================================== |
| # pylint: disable=W0613 |
| |
| 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 |
| |
| |
| # ============================================================================== |
| class rowSpan(nodes.General, nodes.Element): pass # pylint: disable=C0103,C0321 |
| class colSpan(nodes.General, nodes.Element): pass # pylint: disable=C0103,C0321 |
| # ============================================================================== |
| |
| # ============================================================================== |
| 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 |
| , 'widths': directives.positive_int_list |
| , 'fill-cells' : directives.flag } |
| |
| 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() |
| # SDK.CONSOLE() # print --> tableNode.asdom().toprettyxml() |
| if title: |
| tableNode.insert(0, title) |
| return [tableNode] + messages |
| |
| |
| # ============================================================================== |
| 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 |
| |
| def buildTableNode(self): |
| |
| colwidths = self.directive.get_column_widths(self.max_cols) |
| if isinstance(colwidths, tuple): |
| # Since docutils 0.13, get_column_widths returns a (widths, |
| # colwidths) tuple, where widths is a string (i.e. 'auto'). |
| # See https://sourceforge.net/p/docutils/patches/120/. |
| colwidths = colwidths[1] |
| stub_columns = self.directive.options.get('stub-columns', 0) |
| header_rows = self.directive.options.get('header-rows', 0) |
| |
| table = nodes.table() |
| 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 |
| stub_columns = self.directive.options.get('stub-columns', 0) |
| |
| 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 |
| |
| 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 |
| |
| 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) |
| |
| 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() |
| |
| def roundOffTableDefinition(self): |
| u"""Round off the table definition. |
| |
| This method rounds off the table definition in :py:member:`rows`. |
| |
| * This method inserts the needed ``None`` values for the missing cells |
| arising from spanning cells over rows and/or columns. |
| |
| * recount the :py:member:`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: # pylint: disable=W0702 |
| # the user sets ambiguous rowspans |
| pass # SDK.CONSOLE() |
| # 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: # pylint: disable=W0702 |
| # the user sets ambiguous rowspans |
| pass # SDK.CONSOLE() |
| 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()) ) |
| |
| 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 + "]" |
| |
| def parseRowItem(self, rowItem, rowNum): |
| row = [] |
| childNo = 0 |
| error = False |
| cell = None |
| target = None |
| |
| for child in rowItem: |
| if (isinstance(child , nodes.comment) |
| or isinstance(child, 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 |
| |
| 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): |
| 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[:] |