StrictDoc Documentation
strictdoc/backend/sdoc/processor.py
Source file coverage
Path:
strictdoc/backend/sdoc/processor.py
Lines:
223
Non-empty lines:
194
Non-empty lines covered with requirements:
194 / 194 (100.0%)
Functions:
14
Functions covered by requirements:
14 / 14 (100.0%)
1
import os.path
2
import re
3
from typing import Any, Callable, Dict, List, Optional, Union, cast
4
 
5
from textx import TextXSyntaxError, get_model
6
 
7
from strictdoc.backend.sdoc.document_reference import DocumentReference
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 DocumentView
13
from strictdoc.backend.sdoc.models.grammar_element import GrammarElement
14
from strictdoc.backend.sdoc.models.model import (
15
    SDocDocumentFromFileIF,
16
    SDocDocumentIF,
17
    SDocNodeIF,
18
)
19
from strictdoc.backend.sdoc.models.node import (
20
    SDocNode,
21
    SDocNodeField,
22
)
23
from strictdoc.helpers.exception import StrictDocException
24
from strictdoc.helpers.textx import (
25
    SupportsTxPosition,
26
    preserve_source_location_data,
27
)
28
 
29
 
30
class ParseContext:
31
    def __init__(
32
        self, path_to_sdoc_file: Optional[str], migrate_sections: bool = False
33
    ):
34
        self.path_to_sdoc_file: Optional[str] = path_to_sdoc_file
35
        self.path_to_sdoc_dir: Optional[str] = None
36
        if path_to_sdoc_file is not None:
37
            self.path_to_sdoc_dir = os.path.dirname(path_to_sdoc_file)
38
        self.document_grammar: Optional[DocumentGrammar] = None
39
        self.document_reference: DocumentReference = DocumentReference()
40
        self.context_document_reference: DocumentReference = DocumentReference()
41
        self.document_config: Optional[DocumentConfig] = None
42
        self.document_view: Optional[DocumentView] = None
43
        self.document_has_requirements = False
44
 
45
        self.fragments_from_files: List[SDocDocumentFromFileIF] = []
46
        self.migrate_sections: bool = migrate_sections
47
 
48
 
49
class SDocParsingProcessor:
50
    def __init__(self, parse_context: ParseContext) -> None:
51
        self.parse_context: ParseContext = parse_context
52
 
53
    def process_document(self, document: SDocDocument) -> None:
54
        document.grammar = (
55
            self.parse_context.document_grammar
56
            or DocumentGrammar.create_default(
57
                document,
58
                enable_mid=document.config.enable_mid or False,
59
            )
60
        )
61
        document.ng_including_document_reference = (
62
            self.parse_context.context_document_reference
63
        )
64
 
65
    def get_default_processors(self) -> Dict[str, Callable[..., None]]:
66
        return {
67
            "SDocDocument": self.process_document,
68
            "DocumentConfig": self.process_document_config,
69
            "DocumentGrammar": self.process_document_grammar,
70
            "GrammarElement": self.process_document_grammar_element,
71
            "DocumentView": self.process_document_view,
72
            "SDocSection": self.process_section,
73
            "DocumentFromFile": self.process_document_from_file,
74
            "SDocCompositeNode": self.process_requirement,
75
            "SDocNode": self.process_requirement,
76
            "SDocNodeField": self.process_node_field,
77
        }
78
 
79
    def process_document_config(self, document_config: DocumentConfig) -> None:
80
        the_model = get_model(document_config)
81
        line_start, col_start = the_model._tx_parser.pos_to_linecol(
82
            cast(SupportsTxPosition, document_config)._tx_position
83
        )
84
        document_config.ng_line_start = line_start
85
        document_config.ng_col_start = col_start
86
        self.parse_context.document_config = document_config
87
 
88
    def process_document_grammar(
89
        self, document_grammar: DocumentGrammar
90
    ) -> None:
91
        assert self.parse_context.document_config is not None
92
 
93
        if (import_from_file_ := document_grammar.import_from_file) is not None:
94
            if re.search(r"\.\.|[/\\]", import_from_file_):
95
                raise StrictDocException(
96
                    "[GRAMMAR]: "
97
                    "IMPORT_FROM_FILE must not contain any '..', '/', '\\' characters: "
98
                    f"{import_from_file_}."
99
                )
100
 
101
        preserve_source_location_data(document_grammar)
102
        # FIXME: It would be great to move forward and remove this.
103
        if not document_grammar.has_text_element():
104
            document_grammar.add_element_first(
105
                DocumentGrammar.create_default_text_element(
106
                    document_grammar,
107
                    enable_mid=self.parse_context.document_config.enable_mid
108
                    is True,
109
                )
110
            )
111
        self.parse_context.document_grammar = document_grammar
112
 
113
    def process_document_grammar_element(
114
        self, grammar_element: GrammarElement
115
    ) -> None:
116
        preserve_source_location_data(grammar_element)
117
 
118
    def process_document_view(self, document_view: DocumentView) -> None:
119
        self.parse_context.document_view = document_view
120
 
121
        the_model = get_model(document_view)
122
        line_start, col_start = the_model._tx_parser.pos_to_linecol(
123
            cast(SupportsTxPosition, document_view)._tx_position
124
        )
125
        document_view.ng_line_start = line_start
126
        document_view.ng_col_start = col_start
127
 
128
    def process_section(self, _: Any) -> None:
129
        raise StrictDocException("""
130
[SECTION] elements are no longer supported by StrictDoc. See the migration guide for more details:\n"
131
"https://strictdoc.readthedocs.io/en/latest/latest/docs/strictdoc_01_user_guide.html#SECTION-UG-NODE-MIGRATION."
132
""")
133
 
134
    def process_document_from_file(
135
        self, document_from_file: DocumentFromFile
136
    ) -> None:
137
        assert isinstance(document_from_file, DocumentFromFile), (
138
            document_from_file
139
        )
140
 
141
        # Windows paths are backslashes, so using abspath in addition.
142
        assert self.parse_context.path_to_sdoc_dir is not None
143
        resolved_path_to_fragment_file = os.path.abspath(
144
            os.path.join(
145
                self.parse_context.path_to_sdoc_dir, document_from_file.file
146
            )
147
        )
148
        if not resolved_path_to_fragment_file.endswith(".sdoc"):
149
            raise StrictDocException(
150
                '[DOCUMENT_FROM_FILE]: A document file name must have ".sdoc" extension: '
151
                f"{document_from_file.file}."
152
            )
153
        if not os.path.isfile(resolved_path_to_fragment_file):
154
            raise StrictDocException(
155
                "[DOCUMENT_FROM_FILE]: Path to a document file does not exist: "
156
                f"{document_from_file.file}."
157
            )
158
 
159
        document_from_file.ng_document_reference = (
160
            self.parse_context.document_reference
161
        )
162
        document_from_file.resolved_full_path_to_document_file = (
163
            resolved_path_to_fragment_file
164
        )
165
 
166
        self.parse_context.fragments_from_files.append(document_from_file)
167
 
168
    def process_requirement(self, requirement: SDocNode) -> None:
169
        requirement.ng_document_reference = (
170
            self.parse_context.document_reference
171
        )
172
        requirement.ng_including_document_reference = (
173
            self.parse_context.context_document_reference
174
        )
175
 
176
        if requirement.is_normative_node():
177
            self.parse_context.document_has_requirements = True
178
 
179
            cursor: Union[SDocDocumentIF, SDocNodeIF] = requirement.parent
180
            while (
181
                isinstance(cursor, (SDocNodeIF))
182
                and not cursor.ng_has_requirements
183
            ):
184
                cursor.ng_has_requirements = True
185
                cursor = cursor.parent
186
 
187
        assert self.parse_context.document_config is not None
188
        if (
189
            requirement.reserved_title is None
190
            or not self.parse_context.document_config.is_requirement_in_toc()
191
        ) and self.parse_context.document_config.auto_levels:
192
            requirement.ng_resolved_custom_level = "None"
193
 
194
        preserve_source_location_data(requirement)
195
 
196
        # FIXME: Refactor to eliminate the need in such assert.
197
        assert self.parse_context.document_config is not None
198
 
199
        if not self.parse_context.document_config.auto_levels:
200
            if not requirement.ng_resolved_custom_level:
201
                raise StrictDocException(
202
                    f"[{requirement.node_type}].LEVEL field is not provided. "
203
                    "This contradicts to the option "
204
                    "[DOCUMENT].OPTIONS.AUTO_LEVELS set to Off. "
205
                    f"Node: {requirement}"
206
                )
207
 
208
    def process_node_field(self, node_field: SDocNodeField) -> None:
209
        node_field_parts = node_field.parts
210
        if (
211
            isinstance(node_field_parts[0], str)
212
            and node_field_parts[0].strip() == ""
213
        ):
214
            the_model = get_model(node_field)
215
            line_start, col_start = the_model._tx_parser.pos_to_linecol(
216
                cast(SupportsTxPosition, node_field)._tx_position
217
            )
218
            raise TextXSyntaxError(
219
                "Node statement cannot be empty.",
220
                line=line_start,
221
                col=col_start,
222
                filename=self.parse_context.path_to_sdoc_file,
223
            )