Completed
Push — master ( 7959fd...7cfbe2 )
by P.R.
03:01
created

TableNode.generate_output_rows()   D

Complexity

Conditions 8

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 59.2467

Importance

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