StrictDoc Documentation
strictdoc/backend/sdoc/writer.py
Source file coverage
Path:
strictdoc/backend/sdoc/writer.py
Lines:
603
Non-empty lines:
525
Non-empty lines covered with requirements:
525 / 525 (100.0%)
Functions:
10
Functions covered by requirements:
10 / 10 (100.0%)
1
import os.path
2
from pathlib import Path
3
from typing import Dict, List, Optional, Tuple
4
 
5
from strictdoc.backend.markdown.formatter import wrap_md_text
6
from strictdoc.backend.rst.formatter import wrap_rst_text
7
from strictdoc.backend.sdoc.constants import SDocMarkup
8
from strictdoc.backend.sdoc.models.document import SDocDocument
9
from strictdoc.backend.sdoc.models.document_config import DocumentConfig
10
from strictdoc.backend.sdoc.models.document_from_file import DocumentFromFile
11
from strictdoc.backend.sdoc.models.document_grammar import DocumentGrammar
12
from strictdoc.backend.sdoc.models.document_view import (
13
    DefaultViewElement,
14
    ViewElement,
15
)
16
from strictdoc.backend.sdoc.models.grammar_element import (
17
    GrammarElementFieldMultipleChoice,
18
    GrammarElementFieldSingleChoice,
19
    GrammarElementFieldString,
20
    GrammarElementFieldTag,
21
    GrammarElementFieldType,
22
    GrammarElementRelationType,
23
    RequirementFieldType,
24
)
25
from strictdoc.backend.sdoc.models.model import (
26
    SDocElementIF,
27
)
28
from strictdoc.backend.sdoc.models.node import (
29
    SDocCompositeNode,
30
    SDocNode,
31
)
32
from strictdoc.backend.sdoc.models.reference import (
33
    ChildReqReference,
34
    FileReference,
35
    ParentReqReference,
36
    Reference,
37
)
38
from strictdoc.backend.sdoc.node_filter import NodeFilter
39
from strictdoc.core.document_iterator import SDocDocumentIterator
40
from strictdoc.core.document_meta import DocumentMeta
41
from strictdoc.core.project_config import ProjectConfig
42
from strictdoc.helpers.cast import assert_cast
43
from strictdoc.helpers.string import ensure_newline
44
 
45
 
46
class SDWriter:
47
    def __init__(
48
        self,
49
        project_config: ProjectConfig,
50
        node_filter: Optional[NodeFilter] = None,
51
    ) -> None:
52
        self.project_config: ProjectConfig = project_config
53
        self.node_filter: Optional[NodeFilter] = node_filter
54
 
55
    def write_to_file(self, document: SDocDocument) -> None:
56
        document_content, fragments_dict = self.write_with_fragments(document)
57
 
58
        document_meta: DocumentMeta = assert_cast(document.meta, DocumentMeta)
59
 
60
        with open(
61
            document_meta.input_doc_full_path, "w", encoding="utf8"
62
        ) as output_file:
63
            output_file.write(document_content)
64
 
65
        path_to_output_file_dir = os.path.dirname(
66
            document_meta.input_doc_full_path
67
        )
68
        Path(path_to_output_file_dir).mkdir(parents=True, exist_ok=True)
69
 
70
        for fragment_path_, fragment_content_ in fragments_dict.items():
71
            path_to_output_fragment = os.path.join(
72
                path_to_output_file_dir, fragment_path_
73
            )
74
            with open(path_to_output_fragment, "w", encoding="utf8") as file_:
75
                file_.write(fragment_content_)
76
 
77
    def write(self, document: SDocDocument) -> str:
78
        document_output, _ = self.write_with_fragments(document)
79
        return document_output
80
 
81
    def write_with_fragments(
82
        self, document: SDocDocument
83
    ) -> Tuple[str, Dict[str, str]]:
84
        fragments_dict: Dict[str, str] = {}
85
 
86
        document_iterator = SDocDocumentIterator(document)
87
        output = ""
88
 
89
        output += "[DOCUMENT]"
90
        output += "\n"
91
 
92
        if document.mid_permanent or document.config.enable_mid:
93
            output += "MID: "
94
            output += document.reserved_mid
95
            output += "\n"
96
 
97
        output += "TITLE: "
98
        output += document.title
99
        output += "\n"
100
 
101
        document_config: DocumentConfig = document.config
102
        if document_config:
103
            uid = document_config.uid
104
            if uid:
105
                output += f"UID: {uid}"
106
                output += "\n"
107
 
108
            version = document_config.version
109
            if version:
110
                output += f"VERSION: {version}"
111
                output += "\n"
112
 
113
            date = document_config.date
114
            if date is not None:
115
                output += f"DATE: {date}"
116
                output += "\n"
117
 
118
            classification = document_config.classification
119
            if classification is not None:
120
                output += f"CLASSIFICATION: {classification}"
121
                output += "\n"
122
 
123
            requirement_prefix = document_config.requirement_prefix
124
            if requirement_prefix is not None:
125
                output += f"PREFIX: {requirement_prefix}"
126
                output += "\n"
127
 
128
            root = document_config.root
129
            if root is not None:
130
                output += "ROOT: "
131
                output += "True" if root else "False"
132
                output += "\n"
133
 
134
            enable_mid = document_config.enable_mid
135
            relation_field = document_config.relation_field
136
            markup = document_config.markup
137
            auto_levels_specified = document_config.ng_auto_levels_specified
138
            layout = document_config.layout
139
            requirement_style = document_config.requirement_style
140
            requirement_in_toc = document_config.requirement_in_toc
141
            default_view = document_config.default_view
142
 
143
            if (
144
                enable_mid is not None
145
                or relation_field is not None
146
                or markup is not None
147
                or auto_levels_specified
148
                or layout is not None
149
                or requirement_style is not None
150
                or requirement_in_toc is not None
151
                or default_view is not None
152
            ):
153
                output += "OPTIONS:"
154
                output += "\n"
155
 
156
                if enable_mid is not None:
157
                    output += "  ENABLE_MID: "
158
                    output += "True" if enable_mid else "False"
159
                    output += "\n"
160
 
161
                if relation_field is not None:
162
                    output += "  RELATION_FIELD: "
163
                    output += relation_field
164
                    output += "\n"
165
 
166
                if markup is not None:
167
                    output += "  MARKUP: "
168
                    output += markup
169
                    output += "\n"
170
 
171
                if auto_levels_specified:
172
                    output += "  AUTO_LEVELS: "
173
                    output += "On" if document_config.auto_levels else "Off"
174
                    output += "\n"
175
 
176
                if layout is not None:
177
                    output += "  LAYOUT: "
178
                    output += layout
179
                    output += "\n"
180
 
181
                if requirement_style is not None:
182
                    output += "  VIEW_STYLE: "
183
                    output += requirement_style
184
                    output += "\n"
185
 
186
                if requirement_in_toc is not None:
187
                    output += "  NODE_IN_TOC: "
188
                    output += requirement_in_toc
189
                    output += "\n"
190
 
191
                if default_view is not None:
192
                    output += "  DEFAULT_VIEW: "
193
                    output += default_view
194
                    output += "\n"
195
 
196
            custom_metadata = document_config.custom_metadata
197
            if custom_metadata is not None:
198
                output += "METADATA:"
199
                output += "\n"
200
 
201
                for keyvalue_pair in custom_metadata.entries:
202
                    if keyvalue_pair.key is not None:
203
                        output += (
204
                            "  "
205
                            + keyvalue_pair.key
206
                            + ": "
207
                            + keyvalue_pair.get_text_value()
208
                        )
209
                        output += "\n"
210
 
211
        document_view = document.view
212
        assert len(document_view.views) > 0
213
        if not isinstance(document_view.views[0], DefaultViewElement):
214
            views = document_view.views
215
            output += "VIEWS:"
216
            output += "\n"
217
            for view in views:
218
                output += f"- ID: {view.view_id}\n"
219
                if view.name is not None:
220
                    output += f"  NAME: {view.name}\n"
221
                assert len(view.tags) > 0
222
                output += "  TAGS:\n"
223
                for tag in view.tags:
224
                    output += f"  - OBJECT_TYPE: {tag.object_type}\n"
225
                    output += "    VISIBLE_FIELDS:\n"
226
                    for field in tag.visible_fields:
227
                        output += f"    - NAME: {field.name}\n"
228
                        placement = field.placement
229
                        if placement:
230
                            output += f"      PLACEMENT: {placement}\n"
231
                hidden_tags = view.hidden_tags
232
                if hidden_tags is not None and len(hidden_tags) > 0:
233
                    output += "  HIDDEN_TAGS:\n"
234
                    for hidden_tag in hidden_tags:
235
                        output += f"  - {hidden_tag.hidden_tag}\n"
236
 
237
        assert document.grammar is not None
238
        document_grammar: DocumentGrammar = document.grammar
239
        if not document_grammar.is_default:
240
            output += "\n[GRAMMAR]\n"
241
            if document_grammar.import_from_file is not None:
242
                output += (
243
                    f"IMPORT_FROM_FILE: {document_grammar.import_from_file}\n"
244
                )
245
            else:
246
                output += "ELEMENTS:\n"
247
                for element in document_grammar.elements:
248
                    output += "- TAG: "
249
                    output += element.tag
250
                    output += "\n"
251
 
252
                    if (
253
                        element.property_is_composite is not None
254
                        or element.property_prefix is not None
255
                        or element.property_view_style is not None
256
                    ):
257
                        output += "  PROPERTIES:\n"
258
                        if element.property_is_composite is not None:
259
                            output += "    IS_COMPOSITE: "
260
                            output += (
261
                                "True"
262
                                if element.property_is_composite
263
                                else "False"
264
                            )
265
                            output += "\n"
266
                        if element.property_prefix is not None:
267
                            output += "    PREFIX: "
268
                            output += element.property_prefix
269
                            output += "\n"
270
                        if element.property_view_style is not None:
271
                            output += "    VIEW_STYLE: "
272
                            output += element.property_view_style
273
                            output += "\n"
274
 
275
                    output += "  FIELDS:\n"
276
                    for grammar_field in element.fields:
277
                        output += SDWriter._print_grammar_field_type(
278
                            grammar_field
279
                        )
280
 
281
                    relations: List[GrammarElementRelationType] = (
282
                        element.relations
283
                    )
284
 
285
                    if len(relations) > 0:
286
                        output += "  RELATIONS:\n"
287
 
288
                        for element_relation in relations:
289
                            output += (
290
                                f"  - TYPE: {element_relation.relation_type}\n"
291
                            )
292
                            if element_relation.relation_role is not None:
293
                                output += f"    ROLE: {element_relation.relation_role}\n"
294
                            if (
295
                                element_relation.reverse_relation_role
296
                                is not None
297
                            ):
298
                                output += f"    REVERSE_ROLE: {element_relation.reverse_relation_role}\n"
299
 
300
        output += "\n"
301
 
302
        output += self._print_node(
303
            document,
304
            document,
305
            document_iterator,
306
        )
307
        output = output.rstrip()
308
        output += "\n"
309
 
310
        return output, fragments_dict
311
 
312
    def _print_node(
313
        self,
314
        root_node: SDocElementIF,
315
        document: SDocDocument,
316
        document_iterator: SDocDocumentIterator,
317
    ) -> str:
318
        # Currently, auto-generated nodes are never written back to file system.
319
        # We could revisit this in the future.
320
        if root_node.autogen:
321
            return ""
322
 
323
        assert isinstance(document_iterator, SDocDocumentIterator), (
324
            document_iterator
325
        )
326
 
327
        if isinstance(root_node, SDocDocument):
328
            output = ""
329
 
330
            for node_ in root_node.section_contents:
331
                if (
332
                    self.node_filter is not None
333
                    and not self.node_filter.is_whitelisted(node_)
334
                ):
335
                    continue
336
                output += self._print_node(
337
                    node_,
338
                    document,
339
                    document_iterator=document_iterator,
340
                )
341
            return output
342
 
343
        if isinstance(root_node, DocumentFromFile):
344
            document_from_file: DocumentFromFile = assert_cast(
345
                root_node, DocumentFromFile
346
            )
347
            return self._print_document_from_file(document_from_file)
348
 
349
        if isinstance(root_node, SDocNode):
350
            output = ""
351
 
352
            if (
353
                isinstance(root_node, SDocCompositeNode)
354
                or root_node.is_composite
355
            ):
356
                output += "[["
357
                output += root_node.node_type
358
                output += "]]\n"
359
            else:
360
                output += "["
361
                output += root_node.node_type
362
                output += "]\n"
363
 
364
            output += self._print_requirement_fields(
365
                section_content=root_node, document=document
366
            )
367
            output += "\n"
368
 
369
            if (
370
                isinstance(root_node, SDocCompositeNode)
371
                or root_node.is_composite
372
            ):
373
                if root_node.section_contents is not None:
374
                    for node_ in root_node.section_contents:
375
                        if (
376
                            self.node_filter is not None
377
                            and not self.node_filter.is_whitelisted(node_)
378
                        ):
379
                            continue
380
                        output += self._print_node(
381
                            node_, document, document_iterator=document_iterator
382
                        )
383
 
384
                if (
385
                    isinstance(root_node, SDocCompositeNode)
386
                    or root_node.is_composite
387
                ):
388
                    output += "[[/"
389
                    output += root_node.node_type
390
                    output += "]]\n"
391
                    output += "\n"
392
 
393
            return output
394
 
395
        raise AssertionError("Must not reach here")  # pragma: no cover
396
 
397
    @staticmethod
398
    def _print_document_from_file(document_from_file: DocumentFromFile) -> str:
399
        assert isinstance(document_from_file, DocumentFromFile)
400
        output = ""
401
        output += "[DOCUMENT_FROM_FILE]"
402
        output += "\n"
403
 
404
        output += "FILE: "
405
        output += document_from_file.file
406
        output += "\n\n"
407
 
408
        return output
409
 
410
    def _print_requirement_fields(
411
        self, section_content: SDocNode, document: SDocDocument
412
    ) -> str:
413
        assert document.grammar is not None
414
 
415
        output = ""
416
 
417
        current_view: ViewElement = document.view.get_current_view(
418
            self.project_config.view
419
        )
420
        element = document.grammar.elements_by_type[section_content.node_type]
421
 
422
        for element_field in element.fields:
423
            field_name = element_field.title
424
            if field_name not in section_content.ordered_fields_lookup:
425
                if field_name == "MID" and document.config.enable_mid:
426
                    output += "MID: "
427
                    output += section_content.reserved_mid
428
                    output += "\n"
429
                continue
430
            if not current_view.includes_field(
431
                section_content.node_type, field_name
432
            ):
433
                continue
434
            fields = section_content.ordered_fields_lookup[field_name]
435
 
436
            for field in fields:
437
                if not field.is_document_origin():
438
                    continue
439
 
440
                field_value = field.get_text_value()
441
                assert len(field_value) > 0
442
 
443
                if field.is_multiline():
444
                    output += f"{field_name}: >>>"
445
                    output += "\n"
446
                    if field_value != "\n":
447
                        effective_value = field_value
448
                        if self.project_config.document_line_width is not None:
449
                            line_width = self.project_config.document_line_width
450
                            markup = document.config.get_markup()
451
                            if markup == SDocMarkup.MARKDOWN:
452
                                effective_value = wrap_md_text(
453
                                    effective_value, line_width
454
                                )
455
                            else:
456
                                effective_value = wrap_rst_text(
457
                                    effective_value, line_width
458
                                )
459
                        output += ensure_newline(effective_value)
460
                    output += "<<<"
461
                    output += "\n"
462
                else:
463
                    output += f"{field_name}: "
464
                    output += field_value
465
                    output += "\n"
466
 
467
        output += SDWriter._print_requirement_relations(section_content)
468
 
469
        return output
470
 
471
    @staticmethod
472
    def _print_grammar_field_type(
473
        grammar_field: GrammarElementFieldType,
474
    ) -> str:
475
        output = ""
476
        output += "  - TITLE: "
477
        output += grammar_field.title
478
        output += "\n"
479
        if grammar_field.human_title is not None:
480
            output += "    HUMAN_TITLE: "
481
            output += grammar_field.human_title
482
            output += "\n"
483
        output += "    TYPE: "
484
 
485
        if isinstance(grammar_field, GrammarElementFieldString):
486
            output += RequirementFieldType.STRING
487
        elif isinstance(grammar_field, GrammarElementFieldSingleChoice):
488
            output += RequirementFieldType.SINGLE_CHOICE
489
            output += "("
490
            output += ", ".join(grammar_field.get_unprocessed_options())
491
            output += ")"
492
        elif isinstance(grammar_field, GrammarElementFieldMultipleChoice):
493
            output += RequirementFieldType.MULTIPLE_CHOICE
494
            output += "("
495
            output += ", ".join(grammar_field.options)
496
            output += ")"
497
        elif isinstance(grammar_field, GrammarElementFieldTag):
498
            output += RequirementFieldType.TAG
499
        else:
500
            raise NotImplementedError from None  # pragma: no cover
501
 
502
        output += "\n"
503
        output += "    REQUIRED: "
504
        output += "True" if grammar_field.required else "False"
505
        output += "\n"
506
        return output
507
 
508
    @classmethod
509
    def _print_requirement_relations(cls, requirement: SDocNode) -> str:
510
        assert isinstance(requirement, SDocNode)
511
 
512
        if len(requirement.relations) == 0:
513
            return ""
514
 
515
        output = "RELATIONS:\n"
516
 
517
        reference: Reference
518
        for reference in requirement.relations:
519
            output += "- TYPE: "
520
            output += reference.ref_type
521
            output += "\n"
522
 
523
            if isinstance(reference, FileReference):
524
                ref: FileReference = reference
525
                if ref.role is not None:
526
                    output += "  ROLE: "
527
                    output += ref.role
528
                    output += "\n"
529
                file_format = ref.get_file_format()
530
                if file_format:
531
                    output += "  FORMAT: "
532
                    output += file_format
533
                    output += "\n"
534
 
535
                if ref.g_file_entry.g_deprecated_file_path is not None:
536
                    output += "  VALUE: "
537
                else:
538
                    output += "  PATH: "
539
 
540
                output += ref.get_posix_path()
541
                output += "\n"
542
                if (
543
                    ref.g_file_entry.deprecated_function is None
544
                    and ref.g_file_entry.deprecated_clazz is None
545
                    and ref.g_file_entry.element is not None
546
                    and ref.g_file_entry.id is not None
547
                ):
548
                    output += "  ELEMENT: "
549
                    output += ref.g_file_entry.element
550
                    output += "\n"
551
                    output += "  ID: "
552
                    output += ref.g_file_entry.id
553
                    output += "\n"
554
                elif (
555
                    ref.g_file_entry.deprecated_function is None
556
                    and ref.g_file_entry.deprecated_clazz is None
557
                    and ref.g_file_entry.id is not None
558
                    and ref.g_file_entry.line_range is None
559
                ):
560
                    output += "  ID: "
561
                    output += ref.g_file_entry.id
562
                    output += "\n"
563
                elif ref.g_file_entry.line_range is not None:
564
                    output += "  LINE_RANGE: "
565
                    output += str(ref.g_file_entry.line_range[0])
566
                    output += ", "
567
                    output += str(ref.g_file_entry.line_range[1])
568
                    output += "\n"
569
                elif ref.g_file_entry.deprecated_function is not None:
570
                    output += "  FUNCTION: "
571
                    output += str(ref.g_file_entry.deprecated_function)
572
                    output += "\n"
573
                elif ref.g_file_entry.deprecated_clazz is not None:
574
                    output += "  CLASS: "
575
                    output += str(ref.g_file_entry.deprecated_clazz)
576
                    output += "\n"
577
 
578
                if ref.g_file_entry.hash is not None:
579
                    output += "  HASH: "
580
                    output += str(ref.g_file_entry.hash)
581
                    output += "\n"
582
 
583
            elif isinstance(reference, ParentReqReference):
584
                parent_reference: ParentReqReference = reference
585
                output += "  VALUE: "
586
                output += parent_reference.ref_uid
587
                output += "\n"
588
                if parent_reference.role is not None:
589
                    output += "  ROLE: "
590
                    output += parent_reference.role
591
                    output += "\n"
592
            elif isinstance(reference, ChildReqReference):
593
                child_reference: ChildReqReference = reference
594
                output += "  VALUE: "
595
                output += child_reference.ref_uid
596
                output += "\n"
597
                if child_reference.role is not None:
598
                    output += "  ROLE: "
599
                    output += child_reference.role
600
                    output += "\n"
601
            else:
602
                raise AssertionError("Must not reach here.")  # pragma: no cover
603
        return output