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