xref: /OK3568_Linux_fs/kernel/Documentation/sphinx/rstFlatTable.py (revision 4882a59341e53eb6f0b4789bf948001014eff981)
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