162306a36Sopenharmony_ci#!/usr/bin/env python3 262306a36Sopenharmony_ci# -*- coding: utf-8; mode: python -*- 362306a36Sopenharmony_ci# pylint: disable=C0330, R0903, R0912 462306a36Sopenharmony_ci 562306a36Sopenharmony_ciu""" 662306a36Sopenharmony_ci flat-table 762306a36Sopenharmony_ci ~~~~~~~~~~ 862306a36Sopenharmony_ci 962306a36Sopenharmony_ci Implementation of the ``flat-table`` reST-directive. 1062306a36Sopenharmony_ci 1162306a36Sopenharmony_ci :copyright: Copyright (C) 2016 Markus Heiser 1262306a36Sopenharmony_ci :license: GPL Version 2, June 1991 see linux/COPYING for details. 1362306a36Sopenharmony_ci 1462306a36Sopenharmony_ci The ``flat-table`` (:py:class:`FlatTable`) is a double-stage list similar to 1562306a36Sopenharmony_ci the ``list-table`` with some additional features: 1662306a36Sopenharmony_ci 1762306a36Sopenharmony_ci * *column-span*: with the role ``cspan`` a cell can be extended through 1862306a36Sopenharmony_ci additional columns 1962306a36Sopenharmony_ci 2062306a36Sopenharmony_ci * *row-span*: with the role ``rspan`` a cell can be extended through 2162306a36Sopenharmony_ci additional rows 2262306a36Sopenharmony_ci 2362306a36Sopenharmony_ci * *auto span* rightmost cell of a table row over the missing cells on the 2462306a36Sopenharmony_ci right side of that table-row. With Option ``:fill-cells:`` this behavior 2562306a36Sopenharmony_ci can be changed from *auto span* to *auto fill*, which automatically inserts 2662306a36Sopenharmony_ci (empty) cells instead of spanning the last cell. 2762306a36Sopenharmony_ci 2862306a36Sopenharmony_ci Options: 2962306a36Sopenharmony_ci 3062306a36Sopenharmony_ci * header-rows: [int] count of header rows 3162306a36Sopenharmony_ci * stub-columns: [int] count of stub columns 3262306a36Sopenharmony_ci * widths: [[int] [int] ... ] widths of columns 3362306a36Sopenharmony_ci * fill-cells: instead of autospann missing cells, insert missing cells 3462306a36Sopenharmony_ci 3562306a36Sopenharmony_ci roles: 3662306a36Sopenharmony_ci 3762306a36Sopenharmony_ci * cspan: [int] additionale columns (*morecols*) 3862306a36Sopenharmony_ci * rspan: [int] additionale rows (*morerows*) 3962306a36Sopenharmony_ci""" 4062306a36Sopenharmony_ci 4162306a36Sopenharmony_ci# ============================================================================== 4262306a36Sopenharmony_ci# imports 4362306a36Sopenharmony_ci# ============================================================================== 4462306a36Sopenharmony_ci 4562306a36Sopenharmony_cifrom docutils import nodes 4662306a36Sopenharmony_cifrom docutils.parsers.rst import directives, roles 4762306a36Sopenharmony_cifrom docutils.parsers.rst.directives.tables import Table 4862306a36Sopenharmony_cifrom docutils.utils import SystemMessagePropagation 4962306a36Sopenharmony_ci 5062306a36Sopenharmony_ci# ============================================================================== 5162306a36Sopenharmony_ci# common globals 5262306a36Sopenharmony_ci# ============================================================================== 5362306a36Sopenharmony_ci 5462306a36Sopenharmony_ci__version__ = '1.0' 5562306a36Sopenharmony_ci 5662306a36Sopenharmony_ci# ============================================================================== 5762306a36Sopenharmony_cidef setup(app): 5862306a36Sopenharmony_ci# ============================================================================== 5962306a36Sopenharmony_ci 6062306a36Sopenharmony_ci app.add_directive("flat-table", FlatTable) 6162306a36Sopenharmony_ci roles.register_local_role('cspan', c_span) 6262306a36Sopenharmony_ci roles.register_local_role('rspan', r_span) 6362306a36Sopenharmony_ci 6462306a36Sopenharmony_ci return dict( 6562306a36Sopenharmony_ci version = __version__, 6662306a36Sopenharmony_ci parallel_read_safe = True, 6762306a36Sopenharmony_ci parallel_write_safe = True 6862306a36Sopenharmony_ci ) 6962306a36Sopenharmony_ci 7062306a36Sopenharmony_ci# ============================================================================== 7162306a36Sopenharmony_cidef c_span(name, rawtext, text, lineno, inliner, options=None, content=None): 7262306a36Sopenharmony_ci# ============================================================================== 7362306a36Sopenharmony_ci # pylint: disable=W0613 7462306a36Sopenharmony_ci 7562306a36Sopenharmony_ci options = options if options is not None else {} 7662306a36Sopenharmony_ci content = content if content is not None else [] 7762306a36Sopenharmony_ci nodelist = [colSpan(span=int(text))] 7862306a36Sopenharmony_ci msglist = [] 7962306a36Sopenharmony_ci return nodelist, msglist 8062306a36Sopenharmony_ci 8162306a36Sopenharmony_ci# ============================================================================== 8262306a36Sopenharmony_cidef r_span(name, rawtext, text, lineno, inliner, options=None, content=None): 8362306a36Sopenharmony_ci# ============================================================================== 8462306a36Sopenharmony_ci # pylint: disable=W0613 8562306a36Sopenharmony_ci 8662306a36Sopenharmony_ci options = options if options is not None else {} 8762306a36Sopenharmony_ci content = content if content is not None else [] 8862306a36Sopenharmony_ci nodelist = [rowSpan(span=int(text))] 8962306a36Sopenharmony_ci msglist = [] 9062306a36Sopenharmony_ci return nodelist, msglist 9162306a36Sopenharmony_ci 9262306a36Sopenharmony_ci 9362306a36Sopenharmony_ci# ============================================================================== 9462306a36Sopenharmony_ciclass rowSpan(nodes.General, nodes.Element): pass # pylint: disable=C0103,C0321 9562306a36Sopenharmony_ciclass colSpan(nodes.General, nodes.Element): pass # pylint: disable=C0103,C0321 9662306a36Sopenharmony_ci# ============================================================================== 9762306a36Sopenharmony_ci 9862306a36Sopenharmony_ci# ============================================================================== 9962306a36Sopenharmony_ciclass FlatTable(Table): 10062306a36Sopenharmony_ci# ============================================================================== 10162306a36Sopenharmony_ci 10262306a36Sopenharmony_ci u"""FlatTable (``flat-table``) directive""" 10362306a36Sopenharmony_ci 10462306a36Sopenharmony_ci option_spec = { 10562306a36Sopenharmony_ci 'name': directives.unchanged 10662306a36Sopenharmony_ci , 'class': directives.class_option 10762306a36Sopenharmony_ci , 'header-rows': directives.nonnegative_int 10862306a36Sopenharmony_ci , 'stub-columns': directives.nonnegative_int 10962306a36Sopenharmony_ci , 'widths': directives.positive_int_list 11062306a36Sopenharmony_ci , 'fill-cells' : directives.flag } 11162306a36Sopenharmony_ci 11262306a36Sopenharmony_ci def run(self): 11362306a36Sopenharmony_ci 11462306a36Sopenharmony_ci if not self.content: 11562306a36Sopenharmony_ci error = self.state_machine.reporter.error( 11662306a36Sopenharmony_ci 'The "%s" directive is empty; content required.' % self.name, 11762306a36Sopenharmony_ci nodes.literal_block(self.block_text, self.block_text), 11862306a36Sopenharmony_ci line=self.lineno) 11962306a36Sopenharmony_ci return [error] 12062306a36Sopenharmony_ci 12162306a36Sopenharmony_ci title, messages = self.make_title() 12262306a36Sopenharmony_ci node = nodes.Element() # anonymous container for parsing 12362306a36Sopenharmony_ci self.state.nested_parse(self.content, self.content_offset, node) 12462306a36Sopenharmony_ci 12562306a36Sopenharmony_ci tableBuilder = ListTableBuilder(self) 12662306a36Sopenharmony_ci tableBuilder.parseFlatTableNode(node) 12762306a36Sopenharmony_ci tableNode = tableBuilder.buildTableNode() 12862306a36Sopenharmony_ci # SDK.CONSOLE() # print --> tableNode.asdom().toprettyxml() 12962306a36Sopenharmony_ci if title: 13062306a36Sopenharmony_ci tableNode.insert(0, title) 13162306a36Sopenharmony_ci return [tableNode] + messages 13262306a36Sopenharmony_ci 13362306a36Sopenharmony_ci 13462306a36Sopenharmony_ci# ============================================================================== 13562306a36Sopenharmony_ciclass ListTableBuilder(object): 13662306a36Sopenharmony_ci# ============================================================================== 13762306a36Sopenharmony_ci 13862306a36Sopenharmony_ci u"""Builds a table from a double-stage list""" 13962306a36Sopenharmony_ci 14062306a36Sopenharmony_ci def __init__(self, directive): 14162306a36Sopenharmony_ci self.directive = directive 14262306a36Sopenharmony_ci self.rows = [] 14362306a36Sopenharmony_ci self.max_cols = 0 14462306a36Sopenharmony_ci 14562306a36Sopenharmony_ci def buildTableNode(self): 14662306a36Sopenharmony_ci 14762306a36Sopenharmony_ci colwidths = self.directive.get_column_widths(self.max_cols) 14862306a36Sopenharmony_ci if isinstance(colwidths, tuple): 14962306a36Sopenharmony_ci # Since docutils 0.13, get_column_widths returns a (widths, 15062306a36Sopenharmony_ci # colwidths) tuple, where widths is a string (i.e. 'auto'). 15162306a36Sopenharmony_ci # See https://sourceforge.net/p/docutils/patches/120/. 15262306a36Sopenharmony_ci colwidths = colwidths[1] 15362306a36Sopenharmony_ci stub_columns = self.directive.options.get('stub-columns', 0) 15462306a36Sopenharmony_ci header_rows = self.directive.options.get('header-rows', 0) 15562306a36Sopenharmony_ci 15662306a36Sopenharmony_ci table = nodes.table() 15762306a36Sopenharmony_ci tgroup = nodes.tgroup(cols=len(colwidths)) 15862306a36Sopenharmony_ci table += tgroup 15962306a36Sopenharmony_ci 16062306a36Sopenharmony_ci 16162306a36Sopenharmony_ci for colwidth in colwidths: 16262306a36Sopenharmony_ci colspec = nodes.colspec(colwidth=colwidth) 16362306a36Sopenharmony_ci # FIXME: It seems, that the stub method only works well in the 16462306a36Sopenharmony_ci # absence of rowspan (observed by the html builder, the docutils-xml 16562306a36Sopenharmony_ci # build seems OK). This is not extraordinary, because there exists 16662306a36Sopenharmony_ci # no table directive (except *this* flat-table) which allows to 16762306a36Sopenharmony_ci # define coexistent of rowspan and stubs (there was no use-case 16862306a36Sopenharmony_ci # before flat-table). This should be reviewed (later). 16962306a36Sopenharmony_ci if stub_columns: 17062306a36Sopenharmony_ci colspec.attributes['stub'] = 1 17162306a36Sopenharmony_ci stub_columns -= 1 17262306a36Sopenharmony_ci tgroup += colspec 17362306a36Sopenharmony_ci stub_columns = self.directive.options.get('stub-columns', 0) 17462306a36Sopenharmony_ci 17562306a36Sopenharmony_ci if header_rows: 17662306a36Sopenharmony_ci thead = nodes.thead() 17762306a36Sopenharmony_ci tgroup += thead 17862306a36Sopenharmony_ci for row in self.rows[:header_rows]: 17962306a36Sopenharmony_ci thead += self.buildTableRowNode(row) 18062306a36Sopenharmony_ci 18162306a36Sopenharmony_ci tbody = nodes.tbody() 18262306a36Sopenharmony_ci tgroup += tbody 18362306a36Sopenharmony_ci 18462306a36Sopenharmony_ci for row in self.rows[header_rows:]: 18562306a36Sopenharmony_ci tbody += self.buildTableRowNode(row) 18662306a36Sopenharmony_ci return table 18762306a36Sopenharmony_ci 18862306a36Sopenharmony_ci def buildTableRowNode(self, row_data, classes=None): 18962306a36Sopenharmony_ci classes = [] if classes is None else classes 19062306a36Sopenharmony_ci row = nodes.row() 19162306a36Sopenharmony_ci for cell in row_data: 19262306a36Sopenharmony_ci if cell is None: 19362306a36Sopenharmony_ci continue 19462306a36Sopenharmony_ci cspan, rspan, cellElements = cell 19562306a36Sopenharmony_ci 19662306a36Sopenharmony_ci attributes = {"classes" : classes} 19762306a36Sopenharmony_ci if rspan: 19862306a36Sopenharmony_ci attributes['morerows'] = rspan 19962306a36Sopenharmony_ci if cspan: 20062306a36Sopenharmony_ci attributes['morecols'] = cspan 20162306a36Sopenharmony_ci entry = nodes.entry(**attributes) 20262306a36Sopenharmony_ci entry.extend(cellElements) 20362306a36Sopenharmony_ci row += entry 20462306a36Sopenharmony_ci return row 20562306a36Sopenharmony_ci 20662306a36Sopenharmony_ci def raiseError(self, msg): 20762306a36Sopenharmony_ci error = self.directive.state_machine.reporter.error( 20862306a36Sopenharmony_ci msg 20962306a36Sopenharmony_ci , nodes.literal_block(self.directive.block_text 21062306a36Sopenharmony_ci , self.directive.block_text) 21162306a36Sopenharmony_ci , line = self.directive.lineno ) 21262306a36Sopenharmony_ci raise SystemMessagePropagation(error) 21362306a36Sopenharmony_ci 21462306a36Sopenharmony_ci def parseFlatTableNode(self, node): 21562306a36Sopenharmony_ci u"""parses the node from a :py:class:`FlatTable` directive's body""" 21662306a36Sopenharmony_ci 21762306a36Sopenharmony_ci if len(node) != 1 or not isinstance(node[0], nodes.bullet_list): 21862306a36Sopenharmony_ci self.raiseError( 21962306a36Sopenharmony_ci 'Error parsing content block for the "%s" directive: ' 22062306a36Sopenharmony_ci 'exactly one bullet list expected.' % self.directive.name ) 22162306a36Sopenharmony_ci 22262306a36Sopenharmony_ci for rowNum, rowItem in enumerate(node[0]): 22362306a36Sopenharmony_ci row = self.parseRowItem(rowItem, rowNum) 22462306a36Sopenharmony_ci self.rows.append(row) 22562306a36Sopenharmony_ci self.roundOffTableDefinition() 22662306a36Sopenharmony_ci 22762306a36Sopenharmony_ci def roundOffTableDefinition(self): 22862306a36Sopenharmony_ci u"""Round off the table definition. 22962306a36Sopenharmony_ci 23062306a36Sopenharmony_ci This method rounds off the table definition in :py:member:`rows`. 23162306a36Sopenharmony_ci 23262306a36Sopenharmony_ci * This method inserts the needed ``None`` values for the missing cells 23362306a36Sopenharmony_ci arising from spanning cells over rows and/or columns. 23462306a36Sopenharmony_ci 23562306a36Sopenharmony_ci * recount the :py:member:`max_cols` 23662306a36Sopenharmony_ci 23762306a36Sopenharmony_ci * Autospan or fill (option ``fill-cells``) missing cells on the right 23862306a36Sopenharmony_ci side of the table-row 23962306a36Sopenharmony_ci """ 24062306a36Sopenharmony_ci 24162306a36Sopenharmony_ci y = 0 24262306a36Sopenharmony_ci while y < len(self.rows): 24362306a36Sopenharmony_ci x = 0 24462306a36Sopenharmony_ci 24562306a36Sopenharmony_ci while x < len(self.rows[y]): 24662306a36Sopenharmony_ci cell = self.rows[y][x] 24762306a36Sopenharmony_ci if cell is None: 24862306a36Sopenharmony_ci x += 1 24962306a36Sopenharmony_ci continue 25062306a36Sopenharmony_ci cspan, rspan = cell[:2] 25162306a36Sopenharmony_ci # handle colspan in current row 25262306a36Sopenharmony_ci for c in range(cspan): 25362306a36Sopenharmony_ci try: 25462306a36Sopenharmony_ci self.rows[y].insert(x+c+1, None) 25562306a36Sopenharmony_ci except: # pylint: disable=W0702 25662306a36Sopenharmony_ci # the user sets ambiguous rowspans 25762306a36Sopenharmony_ci pass # SDK.CONSOLE() 25862306a36Sopenharmony_ci # handle colspan in spanned rows 25962306a36Sopenharmony_ci for r in range(rspan): 26062306a36Sopenharmony_ci for c in range(cspan + 1): 26162306a36Sopenharmony_ci try: 26262306a36Sopenharmony_ci self.rows[y+r+1].insert(x+c, None) 26362306a36Sopenharmony_ci except: # pylint: disable=W0702 26462306a36Sopenharmony_ci # the user sets ambiguous rowspans 26562306a36Sopenharmony_ci pass # SDK.CONSOLE() 26662306a36Sopenharmony_ci x += 1 26762306a36Sopenharmony_ci y += 1 26862306a36Sopenharmony_ci 26962306a36Sopenharmony_ci # Insert the missing cells on the right side. For this, first 27062306a36Sopenharmony_ci # re-calculate the max columns. 27162306a36Sopenharmony_ci 27262306a36Sopenharmony_ci for row in self.rows: 27362306a36Sopenharmony_ci if self.max_cols < len(row): 27462306a36Sopenharmony_ci self.max_cols = len(row) 27562306a36Sopenharmony_ci 27662306a36Sopenharmony_ci # fill with empty cells or cellspan? 27762306a36Sopenharmony_ci 27862306a36Sopenharmony_ci fill_cells = False 27962306a36Sopenharmony_ci if 'fill-cells' in self.directive.options: 28062306a36Sopenharmony_ci fill_cells = True 28162306a36Sopenharmony_ci 28262306a36Sopenharmony_ci for row in self.rows: 28362306a36Sopenharmony_ci x = self.max_cols - len(row) 28462306a36Sopenharmony_ci if x and not fill_cells: 28562306a36Sopenharmony_ci if row[-1] is None: 28662306a36Sopenharmony_ci row.append( ( x - 1, 0, []) ) 28762306a36Sopenharmony_ci else: 28862306a36Sopenharmony_ci cspan, rspan, content = row[-1] 28962306a36Sopenharmony_ci row[-1] = (cspan + x, rspan, content) 29062306a36Sopenharmony_ci elif x and fill_cells: 29162306a36Sopenharmony_ci for i in range(x): 29262306a36Sopenharmony_ci row.append( (0, 0, nodes.comment()) ) 29362306a36Sopenharmony_ci 29462306a36Sopenharmony_ci def pprint(self): 29562306a36Sopenharmony_ci # for debugging 29662306a36Sopenharmony_ci retVal = "[ " 29762306a36Sopenharmony_ci for row in self.rows: 29862306a36Sopenharmony_ci retVal += "[ " 29962306a36Sopenharmony_ci for col in row: 30062306a36Sopenharmony_ci if col is None: 30162306a36Sopenharmony_ci retVal += ('%r' % col) 30262306a36Sopenharmony_ci retVal += "\n , " 30362306a36Sopenharmony_ci else: 30462306a36Sopenharmony_ci content = col[2][0].astext() 30562306a36Sopenharmony_ci if len (content) > 30: 30662306a36Sopenharmony_ci content = content[:30] + "..." 30762306a36Sopenharmony_ci retVal += ('(cspan=%s, rspan=%s, %r)' 30862306a36Sopenharmony_ci % (col[0], col[1], content)) 30962306a36Sopenharmony_ci retVal += "]\n , " 31062306a36Sopenharmony_ci retVal = retVal[:-2] 31162306a36Sopenharmony_ci retVal += "]\n , " 31262306a36Sopenharmony_ci retVal = retVal[:-2] 31362306a36Sopenharmony_ci return retVal + "]" 31462306a36Sopenharmony_ci 31562306a36Sopenharmony_ci def parseRowItem(self, rowItem, rowNum): 31662306a36Sopenharmony_ci row = [] 31762306a36Sopenharmony_ci childNo = 0 31862306a36Sopenharmony_ci error = False 31962306a36Sopenharmony_ci cell = None 32062306a36Sopenharmony_ci target = None 32162306a36Sopenharmony_ci 32262306a36Sopenharmony_ci for child in rowItem: 32362306a36Sopenharmony_ci if (isinstance(child , nodes.comment) 32462306a36Sopenharmony_ci or isinstance(child, nodes.system_message)): 32562306a36Sopenharmony_ci pass 32662306a36Sopenharmony_ci elif isinstance(child , nodes.target): 32762306a36Sopenharmony_ci target = child 32862306a36Sopenharmony_ci elif isinstance(child, nodes.bullet_list): 32962306a36Sopenharmony_ci childNo += 1 33062306a36Sopenharmony_ci cell = child 33162306a36Sopenharmony_ci else: 33262306a36Sopenharmony_ci error = True 33362306a36Sopenharmony_ci break 33462306a36Sopenharmony_ci 33562306a36Sopenharmony_ci if childNo != 1 or error: 33662306a36Sopenharmony_ci self.raiseError( 33762306a36Sopenharmony_ci 'Error parsing content block for the "%s" directive: ' 33862306a36Sopenharmony_ci 'two-level bullet list expected, but row %s does not ' 33962306a36Sopenharmony_ci 'contain a second-level bullet list.' 34062306a36Sopenharmony_ci % (self.directive.name, rowNum + 1)) 34162306a36Sopenharmony_ci 34262306a36Sopenharmony_ci for cellItem in cell: 34362306a36Sopenharmony_ci cspan, rspan, cellElements = self.parseCellItem(cellItem) 34462306a36Sopenharmony_ci if target is not None: 34562306a36Sopenharmony_ci cellElements.insert(0, target) 34662306a36Sopenharmony_ci row.append( (cspan, rspan, cellElements) ) 34762306a36Sopenharmony_ci return row 34862306a36Sopenharmony_ci 34962306a36Sopenharmony_ci def parseCellItem(self, cellItem): 35062306a36Sopenharmony_ci # search and remove cspan, rspan colspec from the first element in 35162306a36Sopenharmony_ci # this listItem (field). 35262306a36Sopenharmony_ci cspan = rspan = 0 35362306a36Sopenharmony_ci if not len(cellItem): 35462306a36Sopenharmony_ci return cspan, rspan, [] 35562306a36Sopenharmony_ci for elem in cellItem[0]: 35662306a36Sopenharmony_ci if isinstance(elem, colSpan): 35762306a36Sopenharmony_ci cspan = elem.get("span") 35862306a36Sopenharmony_ci elem.parent.remove(elem) 35962306a36Sopenharmony_ci continue 36062306a36Sopenharmony_ci if isinstance(elem, rowSpan): 36162306a36Sopenharmony_ci rspan = elem.get("span") 36262306a36Sopenharmony_ci elem.parent.remove(elem) 36362306a36Sopenharmony_ci continue 36462306a36Sopenharmony_ci return cspan, rspan, cellItem[:] 365