Completed
Push — master ( 4c8e7d...ffbd85 )
by P.R.
01:53
created

TableNode.generate_output_rows()   C

Complexity

Conditions 7

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

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