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