Coverage for trlc/trlc_markdown_parser.py: 80%
69 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-09-30 11:03 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-09-30 11:03 +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 # The H1 heading takes the role of the 'package' keyword, so the
34 # inherited preamble parsing applies unchanged to markdown files.
35 PACKAGE_KEYWORD = "#"
37 # Markdown-friendly aliases that intentionally map to TRLC block tokens.
38 MD_SECTION_START_TOKEN = "C_BRA"
39 MD_SECTION_END_TOKEN = "C_KET"
41 def __init__(
42 self,
43 mh,
44 stab,
45 file_name,
46 lint_mode,
47 error_recovery,
48 primary_file=True,
49 lexer=None,
50 ):
51 super().__init__(
52 mh=mh,
53 stab=stab,
54 file_name=file_name,
55 lint_mode=lint_mode,
56 error_recovery=error_recovery,
57 primary_file=primary_file,
58 lexer=lexer,
59 )
60 if lexer is not None and hasattr(lexer, "KEYWORDS"): 60 ↛ exitline 60 didn't return from function '__init__' because the condition on line 60 was always true
61 self.language_keywords = lexer.KEYWORDS
63 def parse_preamble(self, kind):
64 # markdown files are routed through TRLC flow in Source_Manager
65 assert kind == "trlc"
66 super().parse_preamble(kind)
68 def parse_section_declaration(self):
69 # H2: '## Section Name'
70 self.match_kw("##")
71 t_section = self.ct
72 self.match("STRING")
73 sec = ast.Section(
74 name=self.ct.value,
75 location=self.ct.location,
76 parent=self.section[-1] if self.section else None,
77 )
78 sec.set_ast_link(self.ct)
79 sec.set_ast_link(t_section)
80 self.section.append(sec)
82 self.match(self.MD_SECTION_START_TOKEN)
83 sec.set_ast_link(self.ct)
84 while not self.peek(self.MD_SECTION_END_TOKEN):
85 self.parse_trlc_entry()
86 self.match(self.MD_SECTION_END_TOKEN)
87 sec.set_ast_link(self.ct)
88 self.section.pop()
90 def parse_trlc_entry(self):
91 if self.peek_kw("##"):
92 self.parse_section_declaration()
93 else:
94 self.cu.add_item(self.parse_record_object_declaration())
96 def parse_trlc_file(self):
97 assert self.cu.package is not None
99 ok = True
100 while self.peek_kw("##") or self.peek("IDENTIFIER"):
101 try:
102 self.parse_trlc_entry()
103 except TRLC_Error as err:
104 if not self.error_recovery or err.kind == "lex error": 104 ↛ 105line 104 didn't jump to line 105 because the condition on line 104 was never true
105 raise
107 ok = False
109 # Mirror TRLC recovery style: scan until likely new entry.
110 self.skip_until_newline()
111 while not self.peek_eof(): 111 ↛ 100line 111 didn't jump to line 100 because the condition on line 111 was always true
112 if self.peek_kw("##"): 112 ↛ 113line 112 didn't jump to line 113 because the condition on line 112 was never true
113 break
114 elif self.peek(self.MD_SECTION_END_TOKEN):
115 # Markdown lexer auto-emits section closing tokens.
116 # If we reached one during recovery, consume it to
117 # avoid a secondary "expected end-of-file" error.
118 self.advance()
119 break
120 elif not self.peek("IDENTIFIER"):
121 pass
122 elif self.stab.contains(self.nt.value): 122 ↛ 123line 122 didn't jump to line 123 because the condition on line 122 was never true
123 n_sym = self.stab.lookup_assuming(self.mh, self.nt.value)
124 if isinstance(n_sym, ast.Package):
125 break
126 elif self.cu.package.symbols.contains(self.nt.value): 126 ↛ 127line 126 didn't jump to line 127 because the condition on line 126 was never true
127 n_sym = self.cu.package.symbols.lookup_assuming(
128 self.mh, self.nt.value
129 )
130 if isinstance(n_sym, ast.Record_Type):
131 break
132 self.advance()
133 self.skip_until_newline()
135 # Markdown lexer emits section close tokens at EOF for open H2 blocks.
136 # After recovery from earlier semantic errors, these can remain
137 # unconsumed and otherwise surface as secondary EOF/brace errors.
138 while self.peek(self.MD_SECTION_END_TOKEN):
139 self.match(self.MD_SECTION_END_TOKEN)
141 self.match_eof()
143 for tok in self.lexer.tokens:
144 if tok.kind == "COMMENT": 144 ↛ 145line 144 didn't jump to line 145 because the condition on line 144 was never true
145 self.cu.package.set_ast_link(tok)
147 return ok