1*4882a593Smuzhiyun#!/usr/bin/env python3 2*4882a593Smuzhiyun# -*- coding: utf-8; mode: python -*- 3*4882a593Smuzhiyun# pylint: disable=C0330, R0903, R0912 4*4882a593Smuzhiyun 5*4882a593Smuzhiyunu""" 6*4882a593Smuzhiyun flat-table 7*4882a593Smuzhiyun ~~~~~~~~~~ 8*4882a593Smuzhiyun 9*4882a593Smuzhiyun Implementation of the ``flat-table`` reST-directive. 10*4882a593Smuzhiyun 11*4882a593Smuzhiyun :copyright: Copyright (C) 2016 Markus Heiser 12*4882a593Smuzhiyun :license: GPL Version 2, June 1991 see linux/COPYING for details. 13*4882a593Smuzhiyun 14*4882a593Smuzhiyun The ``flat-table`` (:py:class:`FlatTable`) is a double-stage list similar to 15*4882a593Smuzhiyun the ``list-table`` with some additional features: 16*4882a593Smuzhiyun 17*4882a593Smuzhiyun * *column-span*: with the role ``cspan`` a cell can be extended through 18*4882a593Smuzhiyun additional columns 19*4882a593Smuzhiyun 20*4882a593Smuzhiyun * *row-span*: with the role ``rspan`` a cell can be extended through 21*4882a593Smuzhiyun additional rows 22*4882a593Smuzhiyun 23*4882a593Smuzhiyun * *auto span* rightmost cell of a table row over the missing cells on the 24*4882a593Smuzhiyun right side of that table-row. With Option ``:fill-cells:`` this behavior 25*4882a593Smuzhiyun can changed from *auto span* to *auto fill*, which automaticly inserts 26*4882a593Smuzhiyun (empty) cells instead of spanning the last cell. 27*4882a593Smuzhiyun 28*4882a593Smuzhiyun Options: 29*4882a593Smuzhiyun 30*4882a593Smuzhiyun * header-rows: [int] count of header rows 31*4882a593Smuzhiyun * stub-columns: [int] count of stub columns 32*4882a593Smuzhiyun * widths: [[int] [int] ... ] widths of columns 33*4882a593Smuzhiyun * fill-cells: instead of autospann missing cells, insert missing cells 34*4882a593Smuzhiyun 35*4882a593Smuzhiyun roles: 36*4882a593Smuzhiyun 37*4882a593Smuzhiyun * cspan: [int] additionale columns (*morecols*) 38*4882a593Smuzhiyun * rspan: [int] additionale rows (*morerows*) 39*4882a593Smuzhiyun""" 40*4882a593Smuzhiyun 41*4882a593Smuzhiyun# ============================================================================== 42*4882a593Smuzhiyun# imports 43*4882a593Smuzhiyun# ============================================================================== 44*4882a593Smuzhiyun 45*4882a593Smuzhiyunimport sys 46*4882a593Smuzhiyun 47*4882a593Smuzhiyunfrom docutils import nodes 48*4882a593Smuzhiyunfrom docutils.parsers.rst import directives, roles 49*4882a593Smuzhiyunfrom docutils.parsers.rst.directives.tables import Table 50*4882a593Smuzhiyunfrom docutils.utils import SystemMessagePropagation 51*4882a593Smuzhiyun 52*4882a593Smuzhiyun# ============================================================================== 53*4882a593Smuzhiyun# common globals 54*4882a593Smuzhiyun# ============================================================================== 55*4882a593Smuzhiyun 56*4882a593Smuzhiyun__version__ = '1.0' 57*4882a593Smuzhiyun 58*4882a593SmuzhiyunPY3 = sys.version_info[0] == 3 59*4882a593SmuzhiyunPY2 = sys.version_info[0] == 2 60*4882a593Smuzhiyun 61*4882a593Smuzhiyunif PY3: 62*4882a593Smuzhiyun # pylint: disable=C0103, W0622 63*4882a593Smuzhiyun unicode = str 64*4882a593Smuzhiyun basestring = str 65*4882a593Smuzhiyun 66*4882a593Smuzhiyun# ============================================================================== 67*4882a593Smuzhiyundef setup(app): 68*4882a593Smuzhiyun# ============================================================================== 69*4882a593Smuzhiyun 70*4882a593Smuzhiyun app.add_directive("flat-table", FlatTable) 71*4882a593Smuzhiyun roles.register_local_role('cspan', c_span) 72*4882a593Smuzhiyun roles.register_local_role('rspan', r_span) 73*4882a593Smuzhiyun 74*4882a593Smuzhiyun return dict( 75*4882a593Smuzhiyun version = __version__, 76*4882a593Smuzhiyun parallel_read_safe = True, 77*4882a593Smuzhiyun parallel_write_safe = True 78*4882a593Smuzhiyun ) 79*4882a593Smuzhiyun 80*4882a593Smuzhiyun# ============================================================================== 81*4882a593Smuzhiyundef c_span(name, rawtext, text, lineno, inliner, options=None, content=None): 82*4882a593Smuzhiyun# ============================================================================== 83*4882a593Smuzhiyun # pylint: disable=W0613 84*4882a593Smuzhiyun 85*4882a593Smuzhiyun options = options if options is not None else {} 86*4882a593Smuzhiyun content = content if content is not None else [] 87*4882a593Smuzhiyun nodelist = [colSpan(span=int(text))] 88*4882a593Smuzhiyun msglist = [] 89*4882a593Smuzhiyun return nodelist, msglist 90*4882a593Smuzhiyun 91*4882a593Smuzhiyun# ============================================================================== 92*4882a593Smuzhiyundef r_span(name, rawtext, text, lineno, inliner, options=None, content=None): 93*4882a593Smuzhiyun# ============================================================================== 94*4882a593Smuzhiyun # pylint: disable=W0613 95*4882a593Smuzhiyun 96*4882a593Smuzhiyun options = options if options is not None else {} 97*4882a593Smuzhiyun content = content if content is not None else [] 98*4882a593Smuzhiyun nodelist = [rowSpan(span=int(text))] 99*4882a593Smuzhiyun msglist = [] 100*4882a593Smuzhiyun return nodelist, msglist 101*4882a593Smuzhiyun 102*4882a593Smuzhiyun 103*4882a593Smuzhiyun# ============================================================================== 104*4882a593Smuzhiyunclass rowSpan(nodes.General, nodes.Element): pass # pylint: disable=C0103,C0321 105*4882a593Smuzhiyunclass colSpan(nodes.General, nodes.Element): pass # pylint: disable=C0103,C0321 106*4882a593Smuzhiyun# ============================================================================== 107*4882a593Smuzhiyun 108*4882a593Smuzhiyun# ============================================================================== 109*4882a593Smuzhiyunclass FlatTable(Table): 110*4882a593Smuzhiyun# ============================================================================== 111*4882a593Smuzhiyun 112*4882a593Smuzhiyun u"""FlatTable (``flat-table``) directive""" 113*4882a593Smuzhiyun 114*4882a593Smuzhiyun option_spec = { 115*4882a593Smuzhiyun 'name': directives.unchanged 116*4882a593Smuzhiyun , 'class': directives.class_option 117*4882a593Smuzhiyun , 'header-rows': directives.nonnegative_int 118*4882a593Smuzhiyun , 'stub-columns': directives.nonnegative_int 119*4882a593Smuzhiyun , 'widths': directives.positive_int_list 120*4882a593Smuzhiyun , 'fill-cells' : directives.flag } 121*4882a593Smuzhiyun 122*4882a593Smuzhiyun def run(self): 123*4882a593Smuzhiyun 124*4882a593Smuzhiyun if not self.content: 125*4882a593Smuzhiyun error = self.state_machine.reporter.error( 126*4882a593Smuzhiyun 'The "%s" directive is empty; content required.' % self.name, 127*4882a593Smuzhiyun nodes.literal_block(self.block_text, self.block_text), 128*4882a593Smuzhiyun line=self.lineno) 129*4882a593Smuzhiyun return [error] 130*4882a593Smuzhiyun 131*4882a593Smuzhiyun title, messages = self.make_title() 132*4882a593Smuzhiyun node = nodes.Element() # anonymous container for parsing 133*4882a593Smuzhiyun self.state.nested_parse(self.content, self.content_offset, node) 134*4882a593Smuzhiyun 135*4882a593Smuzhiyun tableBuilder = ListTableBuilder(self) 136*4882a593Smuzhiyun tableBuilder.parseFlatTableNode(node) 137*4882a593Smuzhiyun tableNode = tableBuilder.buildTableNode() 138*4882a593Smuzhiyun # SDK.CONSOLE() # print --> tableNode.asdom().toprettyxml() 139*4882a593Smuzhiyun if title: 140*4882a593Smuzhiyun tableNode.insert(0, title) 141*4882a593Smuzhiyun return [tableNode] + messages 142*4882a593Smuzhiyun 143*4882a593Smuzhiyun 144*4882a593Smuzhiyun# ============================================================================== 145*4882a593Smuzhiyunclass ListTableBuilder(object): 146*4882a593Smuzhiyun# ============================================================================== 147*4882a593Smuzhiyun 148*4882a593Smuzhiyun u"""Builds a table from a double-stage list""" 149*4882a593Smuzhiyun 150*4882a593Smuzhiyun def __init__(self, directive): 151*4882a593Smuzhiyun self.directive = directive 152*4882a593Smuzhiyun self.rows = [] 153*4882a593Smuzhiyun self.max_cols = 0 154*4882a593Smuzhiyun 155*4882a593Smuzhiyun def buildTableNode(self): 156*4882a593Smuzhiyun 157*4882a593Smuzhiyun colwidths = self.directive.get_column_widths(self.max_cols) 158*4882a593Smuzhiyun if isinstance(colwidths, tuple): 159*4882a593Smuzhiyun # Since docutils 0.13, get_column_widths returns a (widths, 160*4882a593Smuzhiyun # colwidths) tuple, where widths is a string (i.e. 'auto'). 161*4882a593Smuzhiyun # See https://sourceforge.net/p/docutils/patches/120/. 162*4882a593Smuzhiyun colwidths = colwidths[1] 163*4882a593Smuzhiyun stub_columns = self.directive.options.get('stub-columns', 0) 164*4882a593Smuzhiyun header_rows = self.directive.options.get('header-rows', 0) 165*4882a593Smuzhiyun 166*4882a593Smuzhiyun table = nodes.table() 167*4882a593Smuzhiyun tgroup = nodes.tgroup(cols=len(colwidths)) 168*4882a593Smuzhiyun table += tgroup 169*4882a593Smuzhiyun 170*4882a593Smuzhiyun 171*4882a593Smuzhiyun for colwidth in colwidths: 172*4882a593Smuzhiyun colspec = nodes.colspec(colwidth=colwidth) 173*4882a593Smuzhiyun # FIXME: It seems, that the stub method only works well in the 174*4882a593Smuzhiyun # absence of rowspan (observed by the html buidler, the docutils-xml 175*4882a593Smuzhiyun # build seems OK). This is not extraordinary, because there exists 176*4882a593Smuzhiyun # no table directive (except *this* flat-table) which allows to 177*4882a593Smuzhiyun # define coexistent of rowspan and stubs (there was no use-case 178*4882a593Smuzhiyun # before flat-table). This should be reviewed (later). 179*4882a593Smuzhiyun if stub_columns: 180*4882a593Smuzhiyun colspec.attributes['stub'] = 1 181*4882a593Smuzhiyun stub_columns -= 1 182*4882a593Smuzhiyun tgroup += colspec 183*4882a593Smuzhiyun stub_columns = self.directive.options.get('stub-columns', 0) 184*4882a593Smuzhiyun 185*4882a593Smuzhiyun if header_rows: 186*4882a593Smuzhiyun thead = nodes.thead() 187*4882a593Smuzhiyun tgroup += thead 188*4882a593Smuzhiyun for row in self.rows[:header_rows]: 189*4882a593Smuzhiyun thead += self.buildTableRowNode(row) 190*4882a593Smuzhiyun 191*4882a593Smuzhiyun tbody = nodes.tbody() 192*4882a593Smuzhiyun tgroup += tbody 193*4882a593Smuzhiyun 194*4882a593Smuzhiyun for row in self.rows[header_rows:]: 195*4882a593Smuzhiyun tbody += self.buildTableRowNode(row) 196*4882a593Smuzhiyun return table 197*4882a593Smuzhiyun 198*4882a593Smuzhiyun def buildTableRowNode(self, row_data, classes=None): 199*4882a593Smuzhiyun classes = [] if classes is None else classes 200*4882a593Smuzhiyun row = nodes.row() 201*4882a593Smuzhiyun for cell in row_data: 202*4882a593Smuzhiyun if cell is None: 203*4882a593Smuzhiyun continue 204*4882a593Smuzhiyun cspan, rspan, cellElements = cell 205*4882a593Smuzhiyun 206*4882a593Smuzhiyun attributes = {"classes" : classes} 207*4882a593Smuzhiyun if rspan: 208*4882a593Smuzhiyun attributes['morerows'] = rspan 209*4882a593Smuzhiyun if cspan: 210*4882a593Smuzhiyun attributes['morecols'] = cspan 211*4882a593Smuzhiyun entry = nodes.entry(**attributes) 212*4882a593Smuzhiyun entry.extend(cellElements) 213*4882a593Smuzhiyun row += entry 214*4882a593Smuzhiyun return row 215*4882a593Smuzhiyun 216*4882a593Smuzhiyun def raiseError(self, msg): 217*4882a593Smuzhiyun error = self.directive.state_machine.reporter.error( 218*4882a593Smuzhiyun msg 219*4882a593Smuzhiyun , nodes.literal_block(self.directive.block_text 220*4882a593Smuzhiyun , self.directive.block_text) 221*4882a593Smuzhiyun , line = self.directive.lineno ) 222*4882a593Smuzhiyun raise SystemMessagePropagation(error) 223*4882a593Smuzhiyun 224*4882a593Smuzhiyun def parseFlatTableNode(self, node): 225*4882a593Smuzhiyun u"""parses the node from a :py:class:`FlatTable` directive's body""" 226*4882a593Smuzhiyun 227*4882a593Smuzhiyun if len(node) != 1 or not isinstance(node[0], nodes.bullet_list): 228*4882a593Smuzhiyun self.raiseError( 229*4882a593Smuzhiyun 'Error parsing content block for the "%s" directive: ' 230*4882a593Smuzhiyun 'exactly one bullet list expected.' % self.directive.name ) 231*4882a593Smuzhiyun 232*4882a593Smuzhiyun for rowNum, rowItem in enumerate(node[0]): 233*4882a593Smuzhiyun row = self.parseRowItem(rowItem, rowNum) 234*4882a593Smuzhiyun self.rows.append(row) 235*4882a593Smuzhiyun self.roundOffTableDefinition() 236*4882a593Smuzhiyun 237*4882a593Smuzhiyun def roundOffTableDefinition(self): 238*4882a593Smuzhiyun u"""Round off the table definition. 239*4882a593Smuzhiyun 240*4882a593Smuzhiyun This method rounds off the table definition in :py:member:`rows`. 241*4882a593Smuzhiyun 242*4882a593Smuzhiyun * This method inserts the needed ``None`` values for the missing cells 243*4882a593Smuzhiyun arising from spanning cells over rows and/or columns. 244*4882a593Smuzhiyun 245*4882a593Smuzhiyun * recount the :py:member:`max_cols` 246*4882a593Smuzhiyun 247*4882a593Smuzhiyun * Autospan or fill (option ``fill-cells``) missing cells on the right 248*4882a593Smuzhiyun side of the table-row 249*4882a593Smuzhiyun """ 250*4882a593Smuzhiyun 251*4882a593Smuzhiyun y = 0 252*4882a593Smuzhiyun while y < len(self.rows): 253*4882a593Smuzhiyun x = 0 254*4882a593Smuzhiyun 255*4882a593Smuzhiyun while x < len(self.rows[y]): 256*4882a593Smuzhiyun cell = self.rows[y][x] 257*4882a593Smuzhiyun if cell is None: 258*4882a593Smuzhiyun x += 1 259*4882a593Smuzhiyun continue 260*4882a593Smuzhiyun cspan, rspan = cell[:2] 261*4882a593Smuzhiyun # handle colspan in current row 262*4882a593Smuzhiyun for c in range(cspan): 263*4882a593Smuzhiyun try: 264*4882a593Smuzhiyun self.rows[y].insert(x+c+1, None) 265*4882a593Smuzhiyun except: # pylint: disable=W0702 266*4882a593Smuzhiyun # the user sets ambiguous rowspans 267*4882a593Smuzhiyun pass # SDK.CONSOLE() 268*4882a593Smuzhiyun # handle colspan in spanned rows 269*4882a593Smuzhiyun for r in range(rspan): 270*4882a593Smuzhiyun for c in range(cspan + 1): 271*4882a593Smuzhiyun try: 272*4882a593Smuzhiyun self.rows[y+r+1].insert(x+c, None) 273*4882a593Smuzhiyun except: # pylint: disable=W0702 274*4882a593Smuzhiyun # the user sets ambiguous rowspans 275*4882a593Smuzhiyun pass # SDK.CONSOLE() 276*4882a593Smuzhiyun x += 1 277*4882a593Smuzhiyun y += 1 278*4882a593Smuzhiyun 279*4882a593Smuzhiyun # Insert the missing cells on the right side. For this, first 280*4882a593Smuzhiyun # re-calculate the max columns. 281*4882a593Smuzhiyun 282*4882a593Smuzhiyun for row in self.rows: 283*4882a593Smuzhiyun if self.max_cols < len(row): 284*4882a593Smuzhiyun self.max_cols = len(row) 285*4882a593Smuzhiyun 286*4882a593Smuzhiyun # fill with empty cells or cellspan? 287*4882a593Smuzhiyun 288*4882a593Smuzhiyun fill_cells = False 289*4882a593Smuzhiyun if 'fill-cells' in self.directive.options: 290*4882a593Smuzhiyun fill_cells = True 291*4882a593Smuzhiyun 292*4882a593Smuzhiyun for row in self.rows: 293*4882a593Smuzhiyun x = self.max_cols - len(row) 294*4882a593Smuzhiyun if x and not fill_cells: 295*4882a593Smuzhiyun if row[-1] is None: 296*4882a593Smuzhiyun row.append( ( x - 1, 0, []) ) 297*4882a593Smuzhiyun else: 298*4882a593Smuzhiyun cspan, rspan, content = row[-1] 299*4882a593Smuzhiyun row[-1] = (cspan + x, rspan, content) 300*4882a593Smuzhiyun elif x and fill_cells: 301*4882a593Smuzhiyun for i in range(x): 302*4882a593Smuzhiyun row.append( (0, 0, nodes.comment()) ) 303*4882a593Smuzhiyun 304*4882a593Smuzhiyun def pprint(self): 305*4882a593Smuzhiyun # for debugging 306*4882a593Smuzhiyun retVal = "[ " 307*4882a593Smuzhiyun for row in self.rows: 308*4882a593Smuzhiyun retVal += "[ " 309*4882a593Smuzhiyun for col in row: 310*4882a593Smuzhiyun if col is None: 311*4882a593Smuzhiyun retVal += ('%r' % col) 312*4882a593Smuzhiyun retVal += "\n , " 313*4882a593Smuzhiyun else: 314*4882a593Smuzhiyun content = col[2][0].astext() 315*4882a593Smuzhiyun if len (content) > 30: 316*4882a593Smuzhiyun content = content[:30] + "..." 317*4882a593Smuzhiyun retVal += ('(cspan=%s, rspan=%s, %r)' 318*4882a593Smuzhiyun % (col[0], col[1], content)) 319*4882a593Smuzhiyun retVal += "]\n , " 320*4882a593Smuzhiyun retVal = retVal[:-2] 321*4882a593Smuzhiyun retVal += "]\n , " 322*4882a593Smuzhiyun retVal = retVal[:-2] 323*4882a593Smuzhiyun return retVal + "]" 324*4882a593Smuzhiyun 325*4882a593Smuzhiyun def parseRowItem(self, rowItem, rowNum): 326*4882a593Smuzhiyun row = [] 327*4882a593Smuzhiyun childNo = 0 328*4882a593Smuzhiyun error = False 329*4882a593Smuzhiyun cell = None 330*4882a593Smuzhiyun target = None 331*4882a593Smuzhiyun 332*4882a593Smuzhiyun for child in rowItem: 333*4882a593Smuzhiyun if (isinstance(child , nodes.comment) 334*4882a593Smuzhiyun or isinstance(child, nodes.system_message)): 335*4882a593Smuzhiyun pass 336*4882a593Smuzhiyun elif isinstance(child , nodes.target): 337*4882a593Smuzhiyun target = child 338*4882a593Smuzhiyun elif isinstance(child, nodes.bullet_list): 339*4882a593Smuzhiyun childNo += 1 340*4882a593Smuzhiyun cell = child 341*4882a593Smuzhiyun else: 342*4882a593Smuzhiyun error = True 343*4882a593Smuzhiyun break 344*4882a593Smuzhiyun 345*4882a593Smuzhiyun if childNo != 1 or error: 346*4882a593Smuzhiyun self.raiseError( 347*4882a593Smuzhiyun 'Error parsing content block for the "%s" directive: ' 348*4882a593Smuzhiyun 'two-level bullet list expected, but row %s does not ' 349*4882a593Smuzhiyun 'contain a second-level bullet list.' 350*4882a593Smuzhiyun % (self.directive.name, rowNum + 1)) 351*4882a593Smuzhiyun 352*4882a593Smuzhiyun for cellItem in cell: 353*4882a593Smuzhiyun cspan, rspan, cellElements = self.parseCellItem(cellItem) 354*4882a593Smuzhiyun if target is not None: 355*4882a593Smuzhiyun cellElements.insert(0, target) 356*4882a593Smuzhiyun row.append( (cspan, rspan, cellElements) ) 357*4882a593Smuzhiyun return row 358*4882a593Smuzhiyun 359*4882a593Smuzhiyun def parseCellItem(self, cellItem): 360*4882a593Smuzhiyun # search and remove cspan, rspan colspec from the first element in 361*4882a593Smuzhiyun # this listItem (field). 362*4882a593Smuzhiyun cspan = rspan = 0 363*4882a593Smuzhiyun if not len(cellItem): 364*4882a593Smuzhiyun return cspan, rspan, [] 365*4882a593Smuzhiyun for elem in cellItem[0]: 366*4882a593Smuzhiyun if isinstance(elem, colSpan): 367*4882a593Smuzhiyun cspan = elem.get("span") 368*4882a593Smuzhiyun elem.parent.remove(elem) 369*4882a593Smuzhiyun continue 370*4882a593Smuzhiyun if isinstance(elem, rowSpan): 371*4882a593Smuzhiyun rspan = elem.get("span") 372*4882a593Smuzhiyun elem.parent.remove(elem) 373*4882a593Smuzhiyun continue 374*4882a593Smuzhiyun return cspan, rspan, cellItem[:] 375