Completed
Pull Request — master (#47)
by Oleg
02:02
created

TableNode.generate_output_rows()   C

Complexity

Conditions 7

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 45.5427

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 26
ccs 1
cts 13
cp 0.0769
rs 5.5
c 1
b 0
f 0
cc 7
crap 45.5427
1
"""
2
SDoc
3
4
Copyright 2016 Set Based IT Consultancy
5
6
Licence MIT
7
"""
8
# ----------------------------------------------------------------------------------------------------------------------
9 1
import csv
10 1
import io
11 1
import re
12
13 1
from sdoc.sdoc2 import in_scope, out_scope
14 1
from sdoc.sdoc2.node.LabelNode import LabelNode
15 1
from sdoc.sdoc2.NodeStore import NodeStore
16 1
from sdoc.sdoc2.node.Node import Node
17
18
19 1
class TableNode(Node):
20
    """
21
    SDoc2 node for table.
22
    """
23
24
    # ------------------------------------------------------------------------------------------------------------------
25 1
    def __init__(self, in_out, options):
26
        """
27
        Object constructor.
28
29
        :param None|cleo.styles.output_style.OutputStyle in_out: The IO object.
30
        :param dict[str,str] options: The options of this table.
31
        """
32
        super().__init__(in_out, 'table', options)
33
34
        self.rows = []
35
        """
36
        The table rows.
37
38
        :type: list[list[str]]
39
        """
40
41
        self.column_headers = []
42
        """
43
        The column headers of the table (if any).
44
45
        :type: list[str]
46
        """
47
48
        self.alignments = []
49
        """
50
        The text alignments in the table columns.
51
52
        :type: list[str|None]
53
        """
54
55
    # ------------------------------------------------------------------------------------------------------------------
56 1
    def get_command(self):
57
        """
58
        Returns the command of this node, i.e. table.
59
60
        :rtype: str
61
        """
62
        return 'table'
63
64
    # ------------------------------------------------------------------------------------------------------------------
65 1
    def is_block_command(self):
66
        """
67
        Returns True.
68
69
        :rtype: bool
70
        """
71
        return True
72
73
    # ------------------------------------------------------------------------------------------------------------------
74 1
    def is_inline_command(self):
75
        """
76
        Returns False.
77
78
        :rtype: bool
79
        """
80
        return False
81
82
    # ------------------------------------------------------------------------------------------------------------------
83 1
    def prepare_content_tree(self):
84
        """
85
        Prepares this node for further processing.
86
        """
87
        table_nodes = []
88
89
        for node_id in self.child_nodes:
90
            node = in_scope(node_id)
91
92
            if not isinstance(node, LabelNode):
93
                table_nodes.append(node)
94
            else:
95
                self.setup_label(node)
96
97
            node.prepare_content_tree()
98
99
            out_scope(node)
100
101
        self.generate_table(table_nodes)
102
103
    # ------------------------------------------------------------------------------------------------------------------
104 1
    def setup_label(self, node):
105
        """
106
        Sets the data of a label to current table.
107
108
        :param sdoc.sdoc2.node.LabelNode.LabelNode node: The label node.
109
        """
110
        self._options['id'] = node.argument
111
112
    # ------------------------------------------------------------------------------------------------------------------
113 1
    def generate_table(self, nodes):
114
        """
115
        Generates the table node.
116
117
        :param list[sdoc.sdoc2.node.Node.Node] nodes: The list with nodes.
118
        """
119
        table_data = TableNode.divide_text_nodes(nodes)
120
121
        splitted_data = TableNode.split_by_new_lines(table_data)
122
123
        rows = self.generate_output_rows(splitted_data)
124
125
        if self.has_header(rows):
126
            self.column_headers = rows[0]
127
            self.rows = rows[2:]
128
            self.alignments = self.get_column_alignments(rows[1])
129
        else:
130
            self.rows = rows
131
132
    # ------------------------------------------------------------------------------------------------------------------
133 1
    @staticmethod
134
    def divide_text_nodes(nodes):
135
        """
136
        Divides text nodes from other type of nodes.
137
138
        :param: list[mixed] nodes: The list with nodes.
139
140
        :rtype: list[mixed]
141
        """
142
        table_data = []
143
        table_text_repr = ''
144
145
        for node in nodes:
146
            if node.get_command() == 'TEXT':
147
                table_text_repr += node.argument
148
            else:
149
                table_data.append(table_text_repr)
150
                table_data.append(node)
151
                table_text_repr = ''
152
153
        if table_text_repr:
154
            table_data.append(table_text_repr)
155
156
        return table_data
157
158
    # ------------------------------------------------------------------------------------------------------------------
159 1
    @staticmethod
160
    def split_by_new_lines(table_data):
161
        """
162
        Splits data by newline symbols.
163
164
        :param list[mixed] table_data:
165
166
        :rtype: list[mixed]
167
        """
168
        splitted_data = []
169
170
        for data in table_data:
171
            if isinstance(data, str):
172
                splitted_data.append(data.split('\n'))
173
            else:
174
                splitted_data.append(data)
175
176
        for data in splitted_data:
177
            if isinstance(data, list):
178
                for element in data:
179
                    if element.isspace() or element == '':
180
                        data.remove(element)
181
182
        return splitted_data
183
184
    # ------------------------------------------------------------------------------------------------------------------
185 1
    def generate_output_rows(self, splitted_data):
186
        """
187
        Generates the rows for final representation.
188
189
        :param list[mixed] splitted_data: The splitted data.
190
191
        :rtype: list[list[mixed]]
192
        """
193
        separated_data = TableNode.split_by_vertical_separators(splitted_data)
194
195
        rows = []
196
197
        for item in separated_data:
198
            row = []
199
200
            for data in item:
201
                if data and isinstance(data, str) and not data.isspace():
202
                    string = io.StringIO(data)
203
                    self.parse_vertical_separators(string, row)
204
                else:
205
                    row.append(data)
206
207
            if row:
208
                rows.append(row)
209
210
        return rows
211
212
    # ------------------------------------------------------------------------------------------------------------------
213 1
    @staticmethod
214
    def split_by_vertical_separators(splitted_data):
215
        """
216
        Splits data by vertical separators and creates rows with data.
217
218
        :param list[mixed] splitted_data: The splitted data.
219
220
        :rtype: list[list[mixed]]
221
        """
222
        rows = []
223
        row = []
224
225
        for item in splitted_data:
226
            # If we have a list we pass for each element.
227
            # In list we have only text elements.
228
            if isinstance(item, list):
229
                for element in item:
230
231
                    # If element starts with '|' we just add element to row.
232
                    if element.strip().startswith('|'):
233
                        row.append(element)
234
235
                    # If element ends with '|' we add row to rows list, reset row to empty list
236
                    # and append there current element. We do it because we know that if string ends with '|',
237
                    # after it we have non-text element.
238
                    elif element.strip().endswith('|'):
239
                        row = TableNode.reset_data(row, rows)
240
                        row.append(element)
241
242
                    # If we have element which not starts and not ends in '|' we do this block.
243
                    else:
244
                        # If last added element of row is not string we add row to rows list, reset row to empty.
245
                        if row and not isinstance(row[-1], str):
246
                            row = TableNode.reset_data(row, rows)
247
                        # And just add full element with text like a row to list of rows.
248
                        rows.append([element])
249
250
            # Do this block if element is not a list.
251
            else:
252
                # If last added element not ends with '|' we add row to rows list, and reset row to empty list.
253
                if row and not row[-1].strip().endswith('|'):
254
                    row = TableNode.reset_data(row, rows)
255
                # Add item to row.
256
                row.append(item)
257
258
        return rows
259
260
    # ------------------------------------------------------------------------------------------------------------------
261 1
    @staticmethod
262
    def reset_data(row, rows):
263
        """
264
        Appends row with data to list of rows, and clear row from any elements.
265
266
        Warning! This method changes original list 'rows'.
267
268
        :param list[mixed] row: The row with elements
269
        :param list[list[mixed]] rows: The list with many rows.
270
        """
271
        rows.append(row)
272
        return []
273
274
    # ------------------------------------------------------------------------------------------------------------------
275 1
    def parse_vertical_separators(self, string, row):
276
        """
277
        Splits row by vertical separator for future output.
278
279
        :param str string: The string which we will separate.
280
        :param list[mixed] row: The list with the row in which we append data.
281
        """
282
        reader = csv.reader(string, delimiter='|')
283
        for line in reader:
284
            new_row = self.prune_whitespace(line)
285
286
            for item in new_row:
287
                if item:
288
                    row.append(item)
289
290
    # ------------------------------------------------------------------------------------------------------------------
291 1
    @staticmethod
292
    def has_header(rows):
293
        """
294
        Returns True if the table has a table header.
295
296
        :param list[str] rows: The second row of the table.
297
298
        :rtype: bool
299
        """
300
        is_header = True
301
302
        if len(rows) == 1:
303
            return False
304
305
        for align in rows[1]:
306
            header_part = re.findall(':?---+-*:?', align)
307
            if not header_part:
308
                is_header = False
309
                break
310
311
        return is_header
312
313
    # ------------------------------------------------------------------------------------------------------------------
314 1
    @staticmethod
315
    def get_column_alignments(row):
316
        """
317
        Sets alignments on table columns.
318
319
        :param list[str] row: The row with hyphens for creating column headers.
320
321
        :rtype: list[str]
322
        """
323
        alignments = []
324
        for hyphens in row:
325
            hyphens = hyphens.strip()
326
            if hyphens.startswith(':') and hyphens.endswith(':'):
327
                alignments.append('center')
328
            elif hyphens.startswith(':'):
329
                alignments.append('left')
330
            elif hyphens.endswith(':'):
331
                alignments.append('right')
332
            else:
333
                alignments.append('')
334
335
        return alignments
336
337
    # ------------------------------------------------------------------------------------------------------------------
338 1
    @staticmethod
339
    def prune_whitespace(row):
340
        """
341
        Strips whitespaces from the text of an each cell.
342
343
        :param list[str] row: The row with text of an each cell.
344
        :rtype: list[str]
345
        """
346
        clear_row = []
347
        for item in row:
348
            clear_text = item.strip()
349
            clear_text = re.sub(r'\s+', ' ', clear_text)
350
            clear_row.append(clear_text)
351
352
        return clear_row
353
354
355
# ----------------------------------------------------------------------------------------------------------------------
356
NodeStore.register_block_command('table', TableNode)
357