| #!/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 | |
| # ============================================================================== | |
| import sys | |
| 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' | |
| PY3 = sys.version_info[0] == 3 | |
| PY2 = sys.version_info[0] == 2 | |
| if PY3: | |
| # pylint: disable=C0103, W0622 | |
| unicode = str | |
| basestring = str | |
| # ============================================================================== | |
| 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[:] |