Completed
Push — master ( 5db226...8f2faa )
by P.R.
03:03
created

NodeStore.__init__()   B

Complexity

Conditions 1

Size

Total Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 46
ccs 8
cts 8
cp 1
rs 8.9411
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:
33
    """
34
    Class for creating, storing, and retrieving nodes.
35
36
    @todo Make abstract and implement other document store classes.
37
    """
38
39
    # ------------------------------------------------------------------------------------------------------------------
40 1
    def __init__(self, io):
41
        """
42
        Object constructor.
43
        """
44
45 1
        self._io = io
46
        """
47
        Styled output formatter.
48
49
        :type: sdoc.style.SdocStyle.SdocStyle
50
        """
51
52 1
        self.format = 'html'
53
        """
54
        The output format.
55
56
        :type: str
57
        """
58
59 1
        self.nested_nodes = []
60
        """
61
        The stack of nested nodes (only filled when creating all nodes).
62
63
        :type: list[sdoc.sdoc2.node.Node.Node]
64
        """
65
66 1
        self.nodes = {}
67
        """
68
        The actual node store. Map from node ID to node.
69
70
        :type: dict[int,sdoc.sdoc2.node.Node.Node]
71
        """
72
73 1
        self._enumerable_numbers = {}
74
        """
75
        The current numbers of enumerable nodes (e.g. headings, figures).
76
77
        :type: dict[str,sdoc.sdoc2.helper.Enumerable.Enumerable]
78
        """
79
80 1
        self.labels = {}
81 1
        """
82
        The identifiers of labels which refers on each heading node.
83
84
        :type: dict[str,str]
85
        """
86
87
    # ------------------------------------------------------------------------------------------------------------------
88 1
    @staticmethod
89
    def get_formatter(output_type, name_formatter):
90
        """
91
        Returns the formatter for special type.
92
93
        :param str output_type: The type of output formatter (e.g. 'html')
94
        :param str name_formatter: The name of formatter (e.g. 'smile')
95
96
        :rtype: sdoc.sdoc2.formatter.Formatter.Formatter
97
        """
98
        return formatters[output_type][name_formatter]
99
100
    # ------------------------------------------------------------------------------------------------------------------
101 1
    def end_block_node(self, command):
102
        """
103
        Signals the end of a block command.
104
105
        :param string command: The name of the inline command.
106
        """
107
        # Pop none block command nodes from the stack.
108 1
        while self.nested_nodes and not self.nested_nodes[-1].is_block_command():
109 1
            self.nested_nodes.pop()
110
111 1
        if not self.nested_nodes:
112
            # @todo position
113
            raise RuntimeError("Unexpected \\end{{{0!s}}}.".format(command))
114
115
        # Get the last node on the block stack.
116 1
        node = self.nested_nodes[-1]
117
118 1
        if node.name != command:
119
            # @todo position \end
120
            # @todo position \begin
121
            raise RuntimeError("\\begin{{{0!s}}} and \\end{{{1!s}}} do not match.".format(node.name, command))
122
123
        # Pop the last node of the block stack.
124 1
        self.nested_nodes.pop()
125
126
    # ------------------------------------------------------------------------------------------------------------------
127 1
    def in_scope(self, node_id):
128
        """
129
        Retrieves a node based on its ID.
130
131
        :param int node_id: The node ID.
132
133
        :rtype: sdoc.sdoc2.node.Node.Node
134
        """
135
        return self.nodes[node_id]
136
137
    # ------------------------------------------------------------------------------------------------------------------
138 1
    def out_scope(self, node):
139
        """
140
        Marks a node as not longer in scope.
141
142
        :param sdoc.sdoc2.node.Node.Node node: The node.
143
        """
144 1
        pass
145
146
    # ------------------------------------------------------------------------------------------------------------------
147 1
    @staticmethod
148
    def register_inline_command(command, constructor):
149
        """
150
        Registers a node constructor for an inline command.
151
152
        :param str command: The name of the inline command.
153
        :param callable constructor: The node constructor.
154
        """
155 1
        inline_creators[command] = constructor
156
157
    # ------------------------------------------------------------------------------------------------------------------
158 1
    @staticmethod
159
    def register_formatter(command, output_format, formatter):
160
        """
161
        Registers a output formatter constructor for a command.
162
163
        :param str command: The name of the command.
164
        :param str output_format: The output format the formatter generates.
165
        :param callable formatter: The formatter for generating the content of the node in the output format.
166
        """
167 1
        if output_format not in formatters:
168 1
            formatters[output_format] = {}
169
170 1
        formatters[output_format][command] = formatter
171
172
    # ------------------------------------------------------------------------------------------------------------------
173 1
    @staticmethod
174
    def register_block_command(command, constructor):
175
        """
176
        Registers a node constructor for a block command.
177
178
        :param string command: The name of the inline command.
179
        :param callable constructor: The node constructor.
180
        """
181 1
        block_creators[command] = constructor
182
183
    # ------------------------------------------------------------------------------------------------------------------
184 1
    def create_inline_node(self, command, options=None, argument='', position=None):
185
        """
186
        Creates a node based an inline command.
187
188
        Note: The node is not stored nor appended to the content tree.
189
190
        :param str command: The inline command.
191
        :param dict options: The options.
192
        :param str argument: The argument of the inline command.
193
        :param None|sdoc.sdoc2.Position.Position position: The position of the node definition.
194
195
        :rtype: sdoc.sdoc2.node.Node.Node
196
        """
197 1
        if command not in inline_creators:
198
            # @todo set error status
199
            constructor = inline_creators['unknown']
200
            node = constructor(self._io, options, argument)
201
            node.name = command
202
203
        else:
204
            # Create the new node.
205 1
            constructor = inline_creators[command]
206 1
            node = constructor(self._io, options, argument)
207
208 1
        node.position = position
209
210
        # Store the node and assign ID.
211 1
        self.store_node(node)
212
213 1
        return node
214
215
    # ------------------------------------------------------------------------------------------------------------------
216 1
    def create_block_node(self, command, options, position=None):
217
        """
218
        Creates a node based on a block command.
219
220
        Note: The node is not appended to the content tree.
221
222
        :param str command: The inline command.
223
        :param dict[str,str] options: The options.
224
        :param None|sdoc.sdoc2.Position.Position position: The position of the node definition.
225
226
        :rtype: sdoc.sdoc2.node.Node.Node
227
        """
228 1
        if command not in block_creators:
229
            constructor = block_creators['unknown']
230
            # @todo set error status
231
232
        else:
233
            # Create the new node.
234 1
            constructor = block_creators[command]
235
236 1
        node = constructor(self._io, options)
237 1
        node.position = position
238
239
        # Store the node and assign ID.
240 1
        self.store_node(node)
241
242 1
        return node
243
244
    # ------------------------------------------------------------------------------------------------------------------
245 1
    def append_inline_node(self, command, options, argument, position):
246
        """
247
        Creates a node based an inline command and appends it to the end of the content tree.
248
249
        :param str command: The inline command.
250
        :param dict options: The options.
251
        :param str argument: The argument of the inline command.
252
        :param sdoc.sdoc2.Position.Position position: The position of the node definition.
253
254
        :rtype: sdoc.sdoc2.node.Node.Node
255
        """
256
        # Create the inline node.
257 1
        node = self.create_inline_node(command, options, argument, position)
258
259
        # Add the node to the node store.
260 1
        self._append_to_content_tree(node)
261
262 1
        return node
263
264
    # ------------------------------------------------------------------------------------------------------------------
265 1
    def append_block_node(self, command, options, position):
266
        """
267
        Creates a node based on a block command and appends it to the end of the content tree.
268
269
        :param str command: The inline command.
270
        :param dict options: The options.
271
        :param sdoc.sdoc2.Position.Position position: The position of the node definition.
272
273
        :rtype: sdoc.sdoc2.node.Node.Node
274
        """
275
        # Create the block node.
276 1
        node = self.create_block_node(command, options, position)
277
278
        # Add the node to the node store.
279 1
        self._append_to_content_tree(node)
280
281 1
        return node
282
283
    # ------------------------------------------------------------------------------------------------------------------
284 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...
285
        """
286
        Creates a formatter for generating the output of nodes in the requested output format.
287
288
        :param cleo.styles.output_style.OutputStyle io: The IO object.
289
        :param str command: The inline of block command.
290
        :param sdoc.sdoc2.formatter.Formatter.Formatter parent: The parent formatter.
291
292
        :rtype: sdoc.sdoc2.formatter.Formatter.Formatter
293
        """
294 1
        if self.format not in formatters:
295
            raise RuntimeError("Unknown output format '{0!s}'.".format(self.format))
296
297 1
        if command not in formatters[self.format]:
298
            # @todo use default none decorator with warning
299
            raise RuntimeError("Unknown formatter '{0!s}' for format '{1!s}'.".format(command, self.format))
300
301 1
        constructor = formatters[self.format][command]
302 1
        formatter = constructor(io, parent)
303
304 1
        return formatter
305
306
    # ------------------------------------------------------------------------------------------------------------------
307 1
    def _adjust_hierarchy(self, node):
308
        """
309
        Adjust the hierarchy based on the hierarchy of a new node.
310
311
        :param sdoc.sdoc2.node.Node.Node node: The new node.
312
        """
313 1
        node_hierarchy_name = node.get_hierarchy_name()
314 1
        parent_found = False
315 1
        while self.nested_nodes and not parent_found:
316 1
            parent_node = self.nested_nodes[-1]
317 1
            parent_hierarchy_name = parent_node.get_hierarchy_name()
318 1
            if parent_hierarchy_name != node_hierarchy_name:
319 1
                if node.is_hierarchy_root():
320 1
                    parent_found = True
321
                else:
322
                    raise RuntimeError("Improper nesting of node '{0!s}' at {1!s} and node '{2!s}' at {3!s}."
323
                                       .format(parent_node.name, parent_node.position, node.name, node.position))
324
325 1
            if not parent_found:
326 1
                parent_hierarchy_level = parent_node.get_hierarchy_level()
327 1
                node_hierarchy_level = node.get_hierarchy_level(parent_hierarchy_level)
328 1
                if parent_hierarchy_level >= node_hierarchy_level and len(self.nested_nodes) > 1:
329 1
                    self.nested_nodes.pop()
330
                else:
331 1
                    parent_found = True
332
333 1
        parent_node = self.nested_nodes[-1]
334 1
        parent_hierarchy_level = parent_node.get_hierarchy_level()
335 1
        node_hierarchy_level = node.get_hierarchy_level(parent_hierarchy_level)
336
337 1
        if node_hierarchy_level - parent_hierarchy_level > 1:
338
            # @todo position
339 1
            self._io.warning(("improper nesting of levels:{0:d} at {1!s} and {2:d} at {3!s}.")
340
                             .format(parent_hierarchy_level, parent_node.position,
341
                                     node_hierarchy_level, node.position))
342
343
    # ------------------------------------------------------------------------------------------------------------------
344 1
    def store_node(self, node):
345
        """
346
        Stores a node. If the node was not stored before assigns an ID to this node, otherwise the node replaces the
347
        node stored under the same ID. Returns the ID if the node.
348
349
        :param sdoc.sdoc2.node.Node.Node node: The node.
350
351
        :rtype: int
352
        """
353 1
        if not node.id:
354
            # Add the node to the node store.
355 1
            node_id = len(self.nodes) + 1
356 1
            node.id = node_id
357
358 1
        self.nodes[node.id] = node
359
360 1
        return node.id
361
362
    # ------------------------------------------------------------------------------------------------------------------
363 1
    def _append_to_content_tree(self, node):
364
        """
365
        Appends the node at the proper nesting level at the end of the content tree.
366
367
        :param sdoc.sdoc2.node.Node.Node node: The node.
368
        """
369 1
        if node.id == 1:
370
            # The first node must be a document root.
371 1
            if not node.is_document_root():
372
                # @todo position of block node.
373
                raise RuntimeError("Node {0!s} is not a document root".format(node.name))
374
375 1
            self.nested_nodes.append(node)
376
377
        else:
378
            # All other nodes must not be a document root.
379 1
            if node.is_document_root():
380
                # @todo position of block node.
381
                raise RuntimeError("Unexpected {0!s}. Node is document root".format(node.name))
382
383
            # If the node is a part of a hierarchy adjust the nested nodes stack.
384 1
            if node.get_hierarchy_name():
385 1
                self._adjust_hierarchy(node)
386
387
            # Add the node to the list of child nodes of its parent node.
388 1
            if self.nested_nodes:
389 1
                parent_node = self.nested_nodes[-1]
390
391
                # Pop from stack if we have two list element nodes (e.g. item nodes) in a row.
392 1
                if node.is_list_element() and type(parent_node) == type(node):
393 1
                    self.nested_nodes.pop()
394 1
                    parent_node = self.nested_nodes[-1]
395
396 1
                parent_node.child_nodes.append(node.id)
397
398
            # Block commands and hierarchical nodes must be appended to the nested nodes.
399 1
            if node.is_block_command() or node.get_hierarchy_name():
400 1
                self.nested_nodes.append(node)
401
402
    # ------------------------------------------------------------------------------------------------------------------
403 1
    def prepare_content_tree(self):
404
        """
405
        Prepares after parsing at SDoc2 level the content tree for further processing.
406
        """
407
        # Currently, node with ID 1 is the document node. @todo Improve getting the document node.
408 1
        self.nodes[1].prepare_content_tree()
409
410
    # ------------------------------------------------------------------------------------------------------------------
411 1
    def number_numerable(self):
412
        """
413
        Numbers all numerable nodes such as chapters, sections, figures, and, items.
414
        """
415 1
        self.nodes[1].number(self._enumerable_numbers)
416
417
    # ------------------------------------------------------------------------------------------------------------------
418 1
    def generate_toc(self):
419
        """
420
        Checks if we have table of contents in document. If yes, we generate table of contents.
421
        """
422 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...
423 1
            if node.get_command() == 'toc':
424
                node.generate_toc()
425
426
    # ------------------------------------------------------------------------------------------------------------------
427 1
    @staticmethod
428
    def generate(target_format):
429
        """
430
        Generates the document.
431
432
        :param sdoc.format.Format.Format target_format: The format which will generate file.
433
        """
434
        # Start generating file using specific formatter.
435 1
        target_format.generate()
436
437 1
        return target_format.errors
438
439
    # ------------------------------------------------------------------------------------------------------------------
440 1
    def get_enumerated_items(self):
441
        """
442
        Returns a list with tuples with command and number of enumerated nodes.
443
444
        This method is intended for unit test only.
445
446
        :rtype: list[(str,str)]
447
        """
448
        return self.nodes[1].get_enumerated_items()
449
450
    # ------------------------------------------------------------------------------------------------------------------
451 1
    def parse_labels(self):
452
        """
453
        Method for parsing labels, setting additional arguments to nodes, and removing label nodes from tree.
454
        """
455 1
        self.nodes[1].parse_labels()
456 1
        self.nodes[1].change_ref_argument()
457
458
# ----------------------------------------------------------------------------------------------------------------------
459