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

NodeStore.out_scope()   A

Complexity

Conditions 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 2
cts 2
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
crap 1
1
"""
2
SDoc
3
4
Copyright 2016 Set Based IT Consultancy
5
6
Licence MIT
7
"""
8
# ----------------------------------------------------------------------------------------------------------------------
9
10 1
inline_creators = {}
0 ignored issues
show
Coding Style Naming introduced by
The name inline_creators does not conform to the constant naming conventions ((([A-Z_][A-Z0-9_]*)|(__.*__))$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
11
"""
12
Map from inline commands to node creators.
13
14
:type: dict[str,callable]
15
"""
16
17 1
block_creators = {}
0 ignored issues
show
Coding Style Naming introduced by
The name block_creators does not conform to the constant naming conventions ((([A-Z_][A-Z0-9_]*)|(__.*__))$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
18
"""
19
Map from block commands to object creators.
20
21
:type: dict[str,callable]
22
"""
23
24 1
formatters = {}
0 ignored issues
show
Coding Style Naming introduced by
The name formatters does not conform to the constant naming conventions ((([A-Z_][A-Z0-9_]*)|(__.*__))$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
25
"""
26
Map from format name to map from inline and block commands to format creators.
27
28
:type: dict[str,dict[str,callable]]
29
"""
30
31
32 1
class NodeStore:
0 ignored issues
show
best-practice introduced by
Too many public methods (22/20)
Loading history...
33
    """
34
    Class for creating, storing, and retrieving nodes.
35
36
    @todo Make abstract and implement other document store classes.
37
    """
38
39 1
    _errors = 0
40
    """
41
    The error count.
42
43
    :type: int
44
    """
45
46 1
    _io = None
47
    """
48
    Styled output formatter.
49
50
    :type: None|sdoc.style.SdocStyle.SdocStyle
51
    """
52
53
    # ------------------------------------------------------------------------------------------------------------------
54 1
    def __init__(self, io):
55
        """
56
        Object constructor.
57
        """
58 1
        NodeStore._io = io
59
60 1
        self.format = 'html'
61
        """
62
        The output format.
63
64
        :type: str
65
        """
66
67 1
        self.nested_nodes = []
68
        """
69
        The stack of nested nodes (only filled when creating all nodes).
70
71
        :type: list[sdoc.sdoc2.node.Node.Node]
72
        """
73
74 1
        self.nodes = {}
75
        """
76
        The actual node store. Map from node ID to node.
77
78
        :type: dict[int,sdoc.sdoc2.node.Node.Node]
79
        """
80
81 1
        self._enumerable_numbers = {}
82
        """
83
        The current numbers of enumerable nodes (e.g. headings, figures).
84
85
        :type: dict[str,sdoc.sdoc2.helper.Enumerable.Enumerable]
86
        """
87
88 1
        self.labels = {}
89
        """
90
        The identifiers of labels which refers on each heading node.
91
92
        :type: dict[str,str]
93
        """
94
95 1
        self._nodes_for_remove = []
96 1
        """
97
        The nodes which we need to remove from node store.
98
99
        :type: list[int]
100
        """
101
102
    # ------------------------------------------------------------------------------------------------------------------
103 1
    @staticmethod
104 1
    def error(message, node=None):
105
        """
106
        Logs an error.
107
108
        :param str message: The error message.this message will be appended with 'at filename:line.column' ot the token.
109
        :param sdoc.sdoc2.node.Node.Node node: The node where the error occurred.
110
        """
111 1
        NodeStore._errors += 1
112
113 1
        messages = [message]
114 1
        if node:
115 1
            filename = node.position.file_name
116 1
            line_number = node.position.start_line
117 1
            column_number = node.position.start_column + 1
118 1
            messages.append('Position: {0!s}:{1:d}.{2:d}'.format(filename, line_number, column_number))
119 1
        NodeStore._io.error(messages)
120
121
    # ------------------------------------------------------------------------------------------------------------------
122 1
    def append_node_for_remove(self, node_id):
123
        """
124
        Append ID of node, to list of nodes, for future removing.
125
126
        :param int node_id: The ID of node which we will remove.
127
        """
128
        self._nodes_for_remove.append(node_id)
129
130
    # ------------------------------------------------------------------------------------------------------------------
131 1
    def remove_nodes(self):
132
        """
133
        Removes nodes from node store listed in 'nodes_for_remove'.
134
        """
135
        # Remove nodes from child nodes of each other node.
136 1
        for node_id in self._nodes_for_remove:
137
            for node in self.nodes:
138
                if node_id in self.nodes[node].child_nodes:
139
                    self.nodes[node].child_nodes.remove(node_id)
140
141
        # Remove nodes from node store.
142 1
        for node_id in self._nodes_for_remove:
143
            del self.nodes[node_id]
144
145
    # ------------------------------------------------------------------------------------------------------------------
146 1
    @staticmethod
147
    def get_formatter(output_type, name_formatter):
148
        """
149
        Returns the formatter for special type.
150
151
        :param str output_type: The type of output formatter (e.g. 'html')
152
        :param str name_formatter: The name of formatter (e.g. 'smile')
153
154
        :rtype: sdoc.sdoc2.formatter.Formatter.Formatter
155
        """
156
        return formatters[output_type][name_formatter]
157
158
    # ------------------------------------------------------------------------------------------------------------------
159 1
    def end_block_node(self, command):
160
        """
161
        Signals the end of a block command.
162
163
        :param string command: The name of the inline command.
164
        """
165
        # Pop none block command nodes from the stack.
166 1
        while self.nested_nodes and not self.nested_nodes[-1].is_block_command():
167 1
            self.nested_nodes.pop()
168
169 1
        if not self.nested_nodes:
170
            # @todo position
171
            raise RuntimeError("Unexpected \\end{{{0!s}}}.".format(command))
172
173
        # Get the last node on the block stack.
174 1
        node = self.nested_nodes[-1]
175
176 1
        if node.name != command:
177
            # @todo position \end
178
            # @todo position \begin
179
            raise RuntimeError("\\begin{{{0!s}}} and \\end{{{1!s}}} do not match.".format(node.name, command))
180
181
        # Pop the last node of the block stack.
182 1
        self.nested_nodes.pop()
183
184
    # ------------------------------------------------------------------------------------------------------------------
185 1
    def in_scope(self, node_id):
186
        """
187
        Retrieves a node based on its ID.
188
189
        :param int node_id: The node ID.
190
191
        :rtype: sdoc.sdoc2.node.Node.Node
192
        """
193
        return self.nodes[node_id]
194
195
    # ------------------------------------------------------------------------------------------------------------------
196 1
    def out_scope(self, node):
197
        """
198
        Marks a node as not longer in scope.
199
200
        :param sdoc.sdoc2.node.Node.Node node: The node.
201
        """
202 1
        pass
203
204
    # ------------------------------------------------------------------------------------------------------------------
205 1
    @staticmethod
206
    def register_inline_command(command, constructor):
207
        """
208
        Registers a node constructor for an inline command.
209
210
        :param str command: The name of the inline command.
211
        :param callable constructor: The node constructor.
212
        """
213 1
        inline_creators[command] = constructor
214
215
    # ------------------------------------------------------------------------------------------------------------------
216 1
    @staticmethod
217
    def register_formatter(command, output_format, formatter):
218
        """
219
        Registers a output formatter constructor for a command.
220
221
        :param str command: The name of the command.
222
        :param str output_format: The output format the formatter generates.
223
        :param callable formatter: The formatter for generating the content of the node in the output format.
224
        """
225 1
        if output_format not in formatters:
226 1
            formatters[output_format] = {}
227
228 1
        formatters[output_format][command] = formatter
229
230
    # ------------------------------------------------------------------------------------------------------------------
231 1
    @staticmethod
232
    def register_block_command(command, constructor):
233
        """
234
        Registers a node constructor for a block command.
235
236
        :param string command: The name of the inline command.
237
        :param callable constructor: The node constructor.
238
        """
239 1
        block_creators[command] = constructor
240
241
    # ------------------------------------------------------------------------------------------------------------------
242 1
    def create_inline_node(self, command, options=None, argument='', position=None):
243
        """
244
        Creates a node based an inline command.
245
246
        Note: The node is not stored nor appended to the content tree.
247
248
        :param str command: The inline command.
249
        :param dict options: The options.
250
        :param str argument: The argument of the inline command.
251
        :param None|sdoc.sdoc2.Position.Position position: The position of the node definition.
252
253
        :rtype: sdoc.sdoc2.node.Node.Node
254
        """
255 1
        if command not in inline_creators:
256
            # @todo set error status
257
            constructor = inline_creators['unknown']
258
            node = constructor(self._io, options, argument)
259
            node.name = command
260
261
        else:
262
            # Create the new node.
263 1
            constructor = inline_creators[command]
264 1
            node = constructor(self._io, options, argument)
265
266 1
        node.position = position
267
268
        # Store the node and assign ID.
269 1
        self.store_node(node)
270
271 1
        return node
272
273
    # ------------------------------------------------------------------------------------------------------------------
274 1
    def create_block_node(self, command, options, position=None):
275
        """
276
        Creates a node based on a block command.
277
278
        Note: The node is not appended to the content tree.
279
280
        :param str command: The inline command.
281
        :param dict[str,str] options: The options.
282
        :param None|sdoc.sdoc2.Position.Position position: The position of the node definition.
283
284
        :rtype: sdoc.sdoc2.node.Node.Node
285
        """
286 1
        if command not in block_creators:
287
            constructor = block_creators['unknown']
288
            # @todo set error status
289
290
        else:
291
            # Create the new node.
292 1
            constructor = block_creators[command]
293
294 1
        node = constructor(self._io, options)
295 1
        node.position = position
296
297
        # Store the node and assign ID.
298 1
        self.store_node(node)
299
300 1
        return node
301
302
    # ------------------------------------------------------------------------------------------------------------------
303 1
    def append_inline_node(self, command, options, argument, position):
304
        """
305
        Creates a node based an inline command and appends it to the end of the content tree.
306
307
        :param str command: The inline command.
308
        :param dict options: The options.
309
        :param str argument: The argument of the inline command.
310
        :param sdoc.sdoc2.Position.Position position: The position of the node definition.
311
312
        :rtype: sdoc.sdoc2.node.Node.Node
313
        """
314
        # Create the inline node.
315 1
        node = self.create_inline_node(command, options, argument, position)
316
317
        # Add the node to the node store.
318 1
        self._append_to_content_tree(node)
319
320 1
        return node
321
322
    # ------------------------------------------------------------------------------------------------------------------
323 1
    def append_block_node(self, command, options, position):
324
        """
325
        Creates a node based on a block command and appends it to the end of the content tree.
326
327
        :param str command: The inline command.
328
        :param dict options: The options.
329
        :param sdoc.sdoc2.Position.Position position: The position of the node definition.
330
331
        :rtype: sdoc.sdoc2.node.Node.Node
332
        """
333
        # Create the block node.
334 1
        node = self.create_block_node(command, options, position)
335
336
        # Add the node to the node store.
337 1
        self._append_to_content_tree(node)
338
339 1
        return node
340
341
    # ------------------------------------------------------------------------------------------------------------------
342 1
    def create_formatter(self, io, command, parent=None):
0 ignored issues
show
Coding Style Naming introduced by
The name io does not conform to the argument naming conventions ([a-z_][a-z0-9_]{2,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
343
        """
344
        Creates a formatter for generating the output of nodes in the requested output format.
345
346
        :param cleo.styles.output_style.OutputStyle io: The IO object.
347
        :param str command: The inline of block command.
348
        :param sdoc.sdoc2.formatter.Formatter.Formatter parent: The parent formatter.
349
350
        :rtype: sdoc.sdoc2.formatter.Formatter.Formatter
351
        """
352 1
        if self.format not in formatters:
353
            raise RuntimeError("Unknown output format '{0!s}'.".format(self.format))
354
355 1
        if command not in formatters[self.format]:
356
            # @todo use default none decorator with warning
357
            raise RuntimeError("Unknown formatter '{0!s}' for format '{1!s}'.".format(command, self.format))
358
359 1
        constructor = formatters[self.format][command]
360 1
        formatter = constructor(io, parent)
361
362 1
        return formatter
363
364
    # ------------------------------------------------------------------------------------------------------------------
365 1
    def _adjust_hierarchy(self, node):
366
        """
367
        Adjust the hierarchy based on the hierarchy of a new node.
368
369
        :param sdoc.sdoc2.node.Node.Node node: The new node.
370
        """
371 1
        node_hierarchy_name = node.get_hierarchy_name()
372 1
        parent_found = False
373 1
        while self.nested_nodes and not parent_found:
374 1
            parent_node = self.nested_nodes[-1]
375 1
            parent_hierarchy_name = parent_node.get_hierarchy_name()
376 1
            if parent_hierarchy_name != node_hierarchy_name:
377 1
                if node.is_hierarchy_root():
378 1
                    parent_found = True
379
                else:
380
                    self.error("Improper nesting of node '{0!s}' at {1!s} and node '{2!s}' at {3!s}.".format(
381
                        parent_node.name, parent_node.position, node.name, node.position))
382
383 1
            if not parent_found:
384 1
                parent_hierarchy_level = parent_node.get_hierarchy_level()
385 1
                node_hierarchy_level = node.get_hierarchy_level(parent_hierarchy_level)
386 1
                if parent_hierarchy_level >= node_hierarchy_level and len(self.nested_nodes) > 1:
387 1
                    self.nested_nodes.pop()
388
                else:
389 1
                    parent_found = True
390
391 1
        parent_node = self.nested_nodes[-1]
392 1
        parent_hierarchy_level = parent_node.get_hierarchy_level()
393 1
        node_hierarchy_level = node.get_hierarchy_level(parent_hierarchy_level)
394
395 1
        if node_hierarchy_level - parent_hierarchy_level > 1:
396 1
            self.error("Improper nesting of levels:{0:d} at {1!s} and {2:d} at {3!s}.".format(
397
                parent_hierarchy_level, parent_node.position, node_hierarchy_level, node.position),
398
                node)
1 ignored issue
show
Coding Style introduced by
Wrong continued indentation.
node)
^ |
Loading history...
399
400
    # ------------------------------------------------------------------------------------------------------------------
401 1
    def store_node(self, node):
402
        """
403
        Stores a node. If the node was not stored before assigns an ID to this node, otherwise the node replaces the
404
        node stored under the same ID. Returns the ID if the node.
405
406
        :param sdoc.sdoc2.node.Node.Node node: The node.
407
408
        :rtype: int
409
        """
410 1
        if not node.id:
411
            # Add the node to the node store.
412 1
            node_id = len(self.nodes) + 1
413 1
            node.id = node_id
414
415 1
        self.nodes[node.id] = node
416
417 1
        return node.id
418
419
    # ------------------------------------------------------------------------------------------------------------------
420 1
    def _append_to_content_tree(self, node):
421
        """
422
        Appends the node at the proper nesting level at the end of the content tree.
423
424
        :param sdoc.sdoc2.node.Node.Node node: The node.
425
        """
426 1
        if node.id == 1:
427
            # The first node must be a document root.
428 1
            if not node.is_document_root():
429
                # @todo position of block node.
430
                raise RuntimeError("Node {0!s} is not a document root".format(node.name))
431
432 1
            self.nested_nodes.append(node)
433
434
        else:
435
            # All other nodes must not be a document root.
436 1
            if node.is_document_root():
437
                # @todo position of block node.
438
                raise RuntimeError("Unexpected {0!s}. Node is document root".format(node.name))
439
440
            # If the node is a part of a hierarchy adjust the nested nodes stack.
441 1
            if node.get_hierarchy_name():
442 1
                self._adjust_hierarchy(node)
443
444
            # Add the node to the list of child nodes of its parent node.
445 1
            if self.nested_nodes:
446 1
                parent_node = self.nested_nodes[-1]
447
448
                # Pop from stack if we have two list element nodes (e.g. item nodes) in a row.
449 1
                if node.is_list_element() and type(parent_node) == type(node):
450 1
                    self.nested_nodes.pop()
451 1
                    parent_node = self.nested_nodes[-1]
452
453 1
                parent_node.child_nodes.append(node.id)
454
455
            # Block commands and hierarchical nodes must be appended to the nested nodes.
456 1
            if node.is_block_command() or node.get_hierarchy_name():
457 1
                self.nested_nodes.append(node)
458
459
    # ------------------------------------------------------------------------------------------------------------------
460 1
    def prepare_content_tree(self):
461
        """
462
        Prepares after parsing at SDoc2 level the content tree for further processing.
463
        """
464
        # Currently, node with ID 1 is the document node. @todo Improve getting the document node.
465 1
        self.nodes[1].prepare_content_tree()
466
467 1
        self.remove_nodes()
468
469
    # ------------------------------------------------------------------------------------------------------------------
470 1
    def number_numerable(self):
471
        """
472
        Numbers all numerable nodes such as chapters, sections, figures, and, items.
473
        """
474 1
        self.nodes[1].number(self._enumerable_numbers)
475
476
    # ------------------------------------------------------------------------------------------------------------------
477 1
    def generate_toc(self):
478
        """
479
        Checks if we have table of contents in document. If yes, we generate table of contents.
480
        """
481 1
        for key, node in self.nodes.items():
0 ignored issues
show
Unused Code introduced by
The variable key seems to be unused.
Loading history...
482 1
            if node.get_command() == 'toc':
483
                node.generate_toc()
484
485
    # ------------------------------------------------------------------------------------------------------------------
486 1
    @staticmethod
487
    def generate(target_format):
488
        """
489
        Generates the document.
490
491
        :param sdoc.format.Format.Format target_format: The format which will generate file.
492
        """
493
        # Start generating file using specific formatter and check the errors.
494 1
        format_errors = target_format.generate()
495
496 1
        NodeStore._errors += format_errors
497
498 1
        return NodeStore._errors
499
500
    # ------------------------------------------------------------------------------------------------------------------
501 1
    def get_enumerated_items(self):
502
        """
503
        Returns a list with tuples with command and number of enumerated nodes.
504
505
        This method is intended for unit test only.
506
507
        :rtype: list[(str,str)]
508
        """
509
        return self.nodes[1].get_enumerated_items()
510
511
    # ------------------------------------------------------------------------------------------------------------------
512 1
    def parse_labels(self):
513
        """
514
        Method for parsing labels, setting additional arguments to nodes, and removing label nodes from tree.
515
        """
516 1
        self.nodes[1].parse_labels()
517 1
        self.nodes[1].change_ref_argument()
518
519
# ----------------------------------------------------------------------------------------------------------------------
520