Completed
Pull Request — master (#43)
by Oleg
02:07
created

NodeStore.generate_toc()   A

Complexity

Conditions 3

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 6.7968

Importance

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