0001
0002
0003
0004
0005 u"""
0006 flat-table
0007 ~~~~~~~~~~
0008
0009 Implementation of the ``flat-table`` reST-directive.
0010
0011 :copyright: Copyright (C) 2016 Markus Heiser
0012 :license: GPL Version 2, June 1991 see linux/COPYING for details.
0013
0014 The ``flat-table`` (:py:class:`FlatTable`) is a double-stage list similar to
0015 the ``list-table`` with some additional features:
0016
0017 * *column-span*: with the role ``cspan`` a cell can be extended through
0018 additional columns
0019
0020 * *row-span*: with the role ``rspan`` a cell can be extended through
0021 additional rows
0022
0023 * *auto span* rightmost cell of a table row over the missing cells on the
0024 right side of that table-row. With Option ``:fill-cells:`` this behavior
0025 can be changed from *auto span* to *auto fill*, which automatically inserts
0026 (empty) cells instead of spanning the last cell.
0027
0028 Options:
0029
0030 * header-rows: [int] count of header rows
0031 * stub-columns: [int] count of stub columns
0032 * widths: [[int] [int] ... ] widths of columns
0033 * fill-cells: instead of autospann missing cells, insert missing cells
0034
0035 roles:
0036
0037 * cspan: [int] additionale columns (*morecols*)
0038 * rspan: [int] additionale rows (*morerows*)
0039 """
0040
0041
0042
0043
0044
0045 from docutils import nodes
0046 from docutils.parsers.rst import directives, roles
0047 from docutils.parsers.rst.directives.tables import Table
0048 from docutils.utils import SystemMessagePropagation
0049
0050
0051
0052
0053
0054 __version__ = '1.0'
0055
0056
0057 def setup(app):
0058
0059
0060 app.add_directive("flat-table", FlatTable)
0061 roles.register_local_role('cspan', c_span)
0062 roles.register_local_role('rspan', r_span)
0063
0064 return dict(
0065 version = __version__,
0066 parallel_read_safe = True,
0067 parallel_write_safe = True
0068 )
0069
0070
0071 def c_span(name, rawtext, text, lineno, inliner, options=None, content=None):
0072
0073
0074
0075 options = options if options is not None else {}
0076 content = content if content is not None else []
0077 nodelist = [colSpan(span=int(text))]
0078 msglist = []
0079 return nodelist, msglist
0080
0081
0082 def r_span(name, rawtext, text, lineno, inliner, options=None, content=None):
0083
0084
0085
0086 options = options if options is not None else {}
0087 content = content if content is not None else []
0088 nodelist = [rowSpan(span=int(text))]
0089 msglist = []
0090 return nodelist, msglist
0091
0092
0093
0094 class rowSpan(nodes.General, nodes.Element): pass
0095 class colSpan(nodes.General, nodes.Element): pass
0096
0097
0098
0099 class FlatTable(Table):
0100
0101
0102 u"""FlatTable (``flat-table``) directive"""
0103
0104 option_spec = {
0105 'name': directives.unchanged
0106 , 'class': directives.class_option
0107 , 'header-rows': directives.nonnegative_int
0108 , 'stub-columns': directives.nonnegative_int
0109 , 'widths': directives.positive_int_list
0110 , 'fill-cells' : directives.flag }
0111
0112 def run(self):
0113
0114 if not self.content:
0115 error = self.state_machine.reporter.error(
0116 'The "%s" directive is empty; content required.' % self.name,
0117 nodes.literal_block(self.block_text, self.block_text),
0118 line=self.lineno)
0119 return [error]
0120
0121 title, messages = self.make_title()
0122 node = nodes.Element()
0123 self.state.nested_parse(self.content, self.content_offset, node)
0124
0125 tableBuilder = ListTableBuilder(self)
0126 tableBuilder.parseFlatTableNode(node)
0127 tableNode = tableBuilder.buildTableNode()
0128
0129 if title:
0130 tableNode.insert(0, title)
0131 return [tableNode] + messages
0132
0133
0134
0135 class ListTableBuilder(object):
0136
0137
0138 u"""Builds a table from a double-stage list"""
0139
0140 def __init__(self, directive):
0141 self.directive = directive
0142 self.rows = []
0143 self.max_cols = 0
0144
0145 def buildTableNode(self):
0146
0147 colwidths = self.directive.get_column_widths(self.max_cols)
0148 if isinstance(colwidths, tuple):
0149
0150
0151
0152 colwidths = colwidths[1]
0153 stub_columns = self.directive.options.get('stub-columns', 0)
0154 header_rows = self.directive.options.get('header-rows', 0)
0155
0156 table = nodes.table()
0157 tgroup = nodes.tgroup(cols=len(colwidths))
0158 table += tgroup
0159
0160
0161 for colwidth in colwidths:
0162 colspec = nodes.colspec(colwidth=colwidth)
0163
0164
0165
0166
0167
0168
0169 if stub_columns:
0170 colspec.attributes['stub'] = 1
0171 stub_columns -= 1
0172 tgroup += colspec
0173 stub_columns = self.directive.options.get('stub-columns', 0)
0174
0175 if header_rows:
0176 thead = nodes.thead()
0177 tgroup += thead
0178 for row in self.rows[:header_rows]:
0179 thead += self.buildTableRowNode(row)
0180
0181 tbody = nodes.tbody()
0182 tgroup += tbody
0183
0184 for row in self.rows[header_rows:]:
0185 tbody += self.buildTableRowNode(row)
0186 return table
0187
0188 def buildTableRowNode(self, row_data, classes=None):
0189 classes = [] if classes is None else classes
0190 row = nodes.row()
0191 for cell in row_data:
0192 if cell is None:
0193 continue
0194 cspan, rspan, cellElements = cell
0195
0196 attributes = {"classes" : classes}
0197 if rspan:
0198 attributes['morerows'] = rspan
0199 if cspan:
0200 attributes['morecols'] = cspan
0201 entry = nodes.entry(**attributes)
0202 entry.extend(cellElements)
0203 row += entry
0204 return row
0205
0206 def raiseError(self, msg):
0207 error = self.directive.state_machine.reporter.error(
0208 msg
0209 , nodes.literal_block(self.directive.block_text
0210 , self.directive.block_text)
0211 , line = self.directive.lineno )
0212 raise SystemMessagePropagation(error)
0213
0214 def parseFlatTableNode(self, node):
0215 u"""parses the node from a :py:class:`FlatTable` directive's body"""
0216
0217 if len(node) != 1 or not isinstance(node[0], nodes.bullet_list):
0218 self.raiseError(
0219 'Error parsing content block for the "%s" directive: '
0220 'exactly one bullet list expected.' % self.directive.name )
0221
0222 for rowNum, rowItem in enumerate(node[0]):
0223 row = self.parseRowItem(rowItem, rowNum)
0224 self.rows.append(row)
0225 self.roundOffTableDefinition()
0226
0227 def roundOffTableDefinition(self):
0228 u"""Round off the table definition.
0229
0230 This method rounds off the table definition in :py:member:`rows`.
0231
0232 * This method inserts the needed ``None`` values for the missing cells
0233 arising from spanning cells over rows and/or columns.
0234
0235 * recount the :py:member:`max_cols`
0236
0237 * Autospan or fill (option ``fill-cells``) missing cells on the right
0238 side of the table-row
0239 """
0240
0241 y = 0
0242 while y < len(self.rows):
0243 x = 0
0244
0245 while x < len(self.rows[y]):
0246 cell = self.rows[y][x]
0247 if cell is None:
0248 x += 1
0249 continue
0250 cspan, rspan = cell[:2]
0251
0252 for c in range(cspan):
0253 try:
0254 self.rows[y].insert(x+c+1, None)
0255 except:
0256
0257 pass
0258
0259 for r in range(rspan):
0260 for c in range(cspan + 1):
0261 try:
0262 self.rows[y+r+1].insert(x+c, None)
0263 except:
0264
0265 pass
0266 x += 1
0267 y += 1
0268
0269
0270
0271
0272 for row in self.rows:
0273 if self.max_cols < len(row):
0274 self.max_cols = len(row)
0275
0276
0277
0278 fill_cells = False
0279 if 'fill-cells' in self.directive.options:
0280 fill_cells = True
0281
0282 for row in self.rows:
0283 x = self.max_cols - len(row)
0284 if x and not fill_cells:
0285 if row[-1] is None:
0286 row.append( ( x - 1, 0, []) )
0287 else:
0288 cspan, rspan, content = row[-1]
0289 row[-1] = (cspan + x, rspan, content)
0290 elif x and fill_cells:
0291 for i in range(x):
0292 row.append( (0, 0, nodes.comment()) )
0293
0294 def pprint(self):
0295
0296 retVal = "[ "
0297 for row in self.rows:
0298 retVal += "[ "
0299 for col in row:
0300 if col is None:
0301 retVal += ('%r' % col)
0302 retVal += "\n , "
0303 else:
0304 content = col[2][0].astext()
0305 if len (content) > 30:
0306 content = content[:30] + "..."
0307 retVal += ('(cspan=%s, rspan=%s, %r)'
0308 % (col[0], col[1], content))
0309 retVal += "]\n , "
0310 retVal = retVal[:-2]
0311 retVal += "]\n , "
0312 retVal = retVal[:-2]
0313 return retVal + "]"
0314
0315 def parseRowItem(self, rowItem, rowNum):
0316 row = []
0317 childNo = 0
0318 error = False
0319 cell = None
0320 target = None
0321
0322 for child in rowItem:
0323 if (isinstance(child , nodes.comment)
0324 or isinstance(child, nodes.system_message)):
0325 pass
0326 elif isinstance(child , nodes.target):
0327 target = child
0328 elif isinstance(child, nodes.bullet_list):
0329 childNo += 1
0330 cell = child
0331 else:
0332 error = True
0333 break
0334
0335 if childNo != 1 or error:
0336 self.raiseError(
0337 'Error parsing content block for the "%s" directive: '
0338 'two-level bullet list expected, but row %s does not '
0339 'contain a second-level bullet list.'
0340 % (self.directive.name, rowNum + 1))
0341
0342 for cellItem in cell:
0343 cspan, rspan, cellElements = self.parseCellItem(cellItem)
0344 if target is not None:
0345 cellElements.insert(0, target)
0346 row.append( (cspan, rspan, cellElements) )
0347 return row
0348
0349 def parseCellItem(self, cellItem):
0350
0351
0352 cspan = rspan = 0
0353 if not len(cellItem):
0354 return cspan, rspan, []
0355 for elem in cellItem[0]:
0356 if isinstance(elem, colSpan):
0357 cspan = elem.get("span")
0358 elem.parent.remove(elem)
0359 continue
0360 if isinstance(elem, rowSpan):
0361 rspan = elem.get("span")
0362 elem.parent.remove(elem)
0363 continue
0364 return cspan, rspan, cellItem[:]