Coverage for trlc/trlc_markdown_parser.py: 68%
84 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-08-12 05:01 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-08-12 05:01 +0000
1#!/usr/bin/env python3
2#
3# TRLC - Treat Requirements Like Code
4# Copyright (C) 2026 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
5#
6# This file is part of the TRLC Python Reference Implementation.
7#
8# TRLC is free software: you can redistribute it and/or modify it
9# under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# TRLC is distributed in the hope that it will be useful, but WITHOUT
14# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
15# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
16# License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with TRLC. If not, see <https://www.gnu.org/licenses/>.
21from trlc import ast
22from trlc.errors import TRLC_Error
23from trlc.parser import Parser
26class TrlcMarkdownParser(Parser):
27 """Parser for .trlc.md files.
29 Reuses TRLC Parser logic for record/object/value semantics and only
30 overrides markdown-specific preamble/section entry points.
31 """
33 # Markdown-friendly aliases that intentionally map to TRLC block tokens.
34 MD_SECTION_START_TOKEN = "C_BRA"
35 MD_SECTION_END_TOKEN = "C_KET"
37 def __init__(
38 self,
39 mh,
40 stab,
41 file_name,
42 lint_mode,
43 error_recovery,
44 primary_file=True,
45 lexer=None,
46 ):
47 super().__init__(
48 mh=mh,
49 stab=stab,
50 file_name=file_name,
51 lint_mode=lint_mode,
52 error_recovery=error_recovery,
53 primary_file=primary_file,
54 lexer=lexer,
55 )
56 if lexer is not None and hasattr(lexer, "KEYWORDS"): 56 ↛ exitline 56 didn't return from function '__init__' because the condition on line 56 was always true
57 self.language_keywords = lexer.KEYWORDS
59 def parse_preamble(self, kind):
60 # markdown files are routed through TRLC flow in Source_Manager
61 assert kind == "trlc"
63 # H1: '# PackageName'
64 self.match_kw("#")
65 t_pkg = self.ct
66 self.match("IDENTIFIER")
68 declare_package = not self.stab.contains(self.ct.value)
69 if declare_package: 69 ↛ 70line 69 didn't jump to line 70 because the condition on line 69 was never true
70 pkg = ast.Package(
71 name=self.ct.value,
72 location=self.ct.location,
73 builtin_stab=self.stab,
74 declared_late=True,
75 )
76 self.stab.register(self.mh, pkg)
77 else:
78 pkg = self.stab.lookup(self.mh, self.ct, ast.Package)
80 pkg.set_ast_link(t_pkg)
81 pkg.set_ast_link(self.ct)
83 self.cu.set_package(pkg)
84 self.default_scope.push(self.cu.package.symbols)
86 # Optional import list right after H1
87 while self.peek_kw("import"): 87 ↛ 88line 87 didn't jump to line 88 because the condition on line 87 was never true
88 self.match_kw("import")
89 pkg.set_ast_link(self.ct)
90 self.match("IDENTIFIER")
91 self.cu.add_import(self.mh, self.ct)
93 def parse_section_declaration(self):
94 # H2: '## Section Name'
95 self.match_kw("##")
96 t_section = self.ct
97 self.match("STRING")
98 sec = ast.Section(
99 name=self.ct.value,
100 location=self.ct.location,
101 parent=self.section[-1] if self.section else None,
102 )
103 sec.set_ast_link(self.ct)
104 sec.set_ast_link(t_section)
105 self.section.append(sec)
107 self.match(self.MD_SECTION_START_TOKEN)
108 sec.set_ast_link(self.ct)
109 while not self.peek(self.MD_SECTION_END_TOKEN):
110 self.parse_trlc_entry()
111 self.match(self.MD_SECTION_END_TOKEN)
112 sec.set_ast_link(self.ct)
113 self.section.pop()
115 def parse_trlc_entry(self):
116 if self.peek_kw("##"):
117 self.parse_section_declaration()
118 else:
119 self.cu.add_item(self.parse_record_object_declaration())
121 def parse_trlc_file(self):
122 assert self.cu.package is not None
124 ok = True
125 while self.peek_kw("##") or self.peek("IDENTIFIER"):
126 try:
127 self.parse_trlc_entry()
128 except TRLC_Error as err:
129 if not self.error_recovery or err.kind == "lex error": 129 ↛ 130line 129 didn't jump to line 130 because the condition on line 129 was never true
130 raise
132 ok = False
134 # Mirror TRLC recovery style: scan until likely new entry.
135 self.skip_until_newline()
136 while not self.peek_eof(): 136 ↛ 125line 136 didn't jump to line 125 because the condition on line 136 was always true
137 if self.peek_kw("##"): 137 ↛ 138line 137 didn't jump to line 138 because the condition on line 137 was never true
138 break
139 elif self.peek(self.MD_SECTION_END_TOKEN): 139 ↛ 145line 139 didn't jump to line 145 because the condition on line 139 was always true
140 # Markdown lexer auto-emits section closing tokens.
141 # If we reached one during recovery, consume it to
142 # avoid a secondary "expected end-of-file" error.
143 self.advance()
144 break
145 elif not self.peek("IDENTIFIER"):
146 pass
147 elif self.stab.contains(self.nt.value):
148 n_sym = self.stab.lookup_assuming(self.mh, self.nt.value)
149 if isinstance(n_sym, ast.Package):
150 break
151 elif self.cu.package.symbols.contains(self.nt.value):
152 n_sym = self.cu.package.symbols.lookup_assuming(
153 self.mh, self.nt.value)
154 if isinstance(n_sym, ast.Record_Type):
155 break
156 self.advance()
157 self.skip_until_newline()
159 # Markdown lexer emits section close tokens at EOF for open H2 blocks.
160 # After recovery from earlier semantic errors, these can remain
161 # unconsumed and otherwise surface as secondary EOF/brace errors.
162 while self.peek(self.MD_SECTION_END_TOKEN):
163 self.match(self.MD_SECTION_END_TOKEN)
165 self.match_eof()
167 for tok in self.lexer.tokens:
168 if tok.kind == "COMMENT": 168 ↛ 169line 168 didn't jump to line 169 because the condition on line 168 was never true
169 self.cu.package.set_ast_link(tok)
171 return ok