Completed
Pull Request — master (#58)
by Oleg
09:19
created

TableNode.number()   A

Complexity

Conditions 2

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 4.6796

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 16
ccs 1
cts 8
cp 0.125
rs 9.4285
cc 2
crap 4.6796
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 not data:
238
                        pass
239
                    else:
240
                        row.append(data)
241
242
            if row:
243
                rows.append(row)
244
245
        return rows
246
247
    # ------------------------------------------------------------------------------------------------------------------
248 1
    @staticmethod
249
    def split_by_vertical_separators(splitted_data):
250
        """
251
        Splits data by vertical separators and creates rows with data.
252
253
        :param list[mixed] splitted_data: The splitted data.
254
255
        :rtype: list[list[mixed]]
256
        """
257
        rows = []
258
        row = []
259
260
        for item in splitted_data:
261
            # If we have a list we pass for each element.
262
            # In list we have only text elements.
263
            if isinstance(item, list):
264
                for element in item:
265
266
                    # If element starts with '|' we just add element to row.
267
                    if element.strip().startswith('|'):
268
                        row.append(element)
269
270
                    # If element ends with '|' we add row to rows list, reset row to empty list
271
                    # and append there current element. We do it because we know that if string ends with '|',
272
                    # after it we have non-text element.
273
                    elif element.strip().endswith('|'):
274
                        row = TableNode.reset_data(row, rows)
275
                        row.append(element)
276
277
                    # If we have element which not starts and not ends in '|' we do this block.
278
                    else:
279
                        # If last added element of row is not string we add row to rows list, reset row to empty.
280
                        if row and not isinstance(row[-1], str):
281
                            row = TableNode.reset_data(row, rows)
282
                        # And just add full element with text like a row to list of rows.
283
                        rows.append([element])
284
285
            # Do this block if element is not a list.
286
            else:
287
                # If last added element not ends with '|' we add row to rows list, and reset row to empty list.
288
                if row and not row[-1].strip().endswith('|'):
289
                    row = TableNode.reset_data(row, rows)
290
                # Add item to row.
291
                row.append(item)
292
293
        return rows
294
295
    # ------------------------------------------------------------------------------------------------------------------
296 1
    @staticmethod
297
    def reset_data(row, rows):
298
        """
299
        Appends row with data to list of rows, and clear row from any elements.
300
301
        Warning! This method changes original list 'rows'.
302
303
        :param list[mixed] row: The row with elements
304
        :param list[list[mixed]] rows: The list with many rows.
305
        """
306
        rows.append(row)
307
        return []
308
309
    # ------------------------------------------------------------------------------------------------------------------
310 1
    def parse_vertical_separators(self, string, row):
311
        """
312
        Splits row by vertical separator for future output.
313
314
        :param str string: The string which we will separate.
315
        :param list[mixed] row: The list with the row in which we append data.
316
        """
317
        reader = csv.reader(string, delimiter='|')
318
        for line in reader:
319
            new_row = self.prune_whitespace(line)
320
321
            for item in new_row:
322
                if item:
323
                    row.append(item)
324
325
    # ------------------------------------------------------------------------------------------------------------------
326 1
    @staticmethod
327
    def has_header(rows):
328
        """
329
        Returns True if the table has a table header.
330
331
        :param list[str] rows: The second row of the table.
332
333
        :rtype: bool
334
        """
335
        is_header = True
336
337
        if len(rows) == 1:
338
            return False
339
340
        for align in rows[1]:
341
            header_part = re.findall(':?---+-*:?', align)
342
            if not header_part:
343
                is_header = False
344
                break
345
346
        return is_header
347
348
    # ------------------------------------------------------------------------------------------------------------------
349 1
    @staticmethod
350
    def get_column_alignments(row):
351
        """
352
        Sets alignments on table columns.
353
354
        :param list[str] row: The row with hyphens for creating column headers.
355
356
        :rtype: list[str]
357
        """
358
        alignments = []
359
        for hyphens in row:
360
            hyphens = hyphens.strip()
361
            if hyphens.startswith(':') and hyphens.endswith(':'):
362
                alignments.append('center')
363
            elif hyphens.startswith(':'):
364
                alignments.append('left')
365
            elif hyphens.endswith(':'):
366
                alignments.append('right')
367
            else:
368
                alignments.append('')
369
370
        return alignments
371
372
    # ------------------------------------------------------------------------------------------------------------------
373 1
    @staticmethod
374
    def prune_whitespace(row):
375
        """
376
        Strips whitespaces from the text of an each cell.
377
378
        :param list[str] row: The row with text of an each cell.
379
        :rtype: list[str]
380
        """
381
        clear_row = []
382
        for item in row:
383
            clear_text = item.strip()
384
            clear_text = re.sub(r'\s+', ' ', clear_text)
385
            clear_row.append(clear_text)
386
387
        return clear_row
388
389
390
# ----------------------------------------------------------------------------------------------------------------------
391
NodeStore.register_block_command('table', TableNode)
392