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

TableNode.split_by_new_lines()   D

Complexity

Conditions 8

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 57.3016

Importance

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