Passed
Push — master ( 8ff2fc...2dab2b )
by P.R.
04:18 queued 10s
created

sdoc.sdoc2.NodeStore.NodeStore.get_formatter()   A

Complexity

Conditions 1

Size

Total Lines 11
Code Lines 3

Duplication

Lines 11
Ratio 100 %

Code Coverage

Tests 2
CRAP Score 1.037

Importance

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