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

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/>. 

20 

21from trlc import ast 

22from trlc.errors import TRLC_Error 

23from trlc.parser import Parser 

24 

25 

26class TrlcMarkdownParser(Parser): 

27 """Parser for .trlc.md files. 

28 

29 Reuses TRLC Parser logic for record/object/value semantics and only 

30 overrides markdown-specific preamble/section entry points. 

31 """ 

32 

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" 

36 

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 

58 

59 def parse_preamble(self, kind): 

60 # markdown files are routed through TRLC flow in Source_Manager 

61 assert kind == "trlc" 

62 

63 # H1: '# PackageName' 

64 self.match_kw("#") 

65 t_pkg = self.ct 

66 self.match("IDENTIFIER") 

67 

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) 

79 

80 pkg.set_ast_link(t_pkg) 

81 pkg.set_ast_link(self.ct) 

82 

83 self.cu.set_package(pkg) 

84 self.default_scope.push(self.cu.package.symbols) 

85 

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) 

92 

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) 

106 

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() 

114 

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()) 

120 

121 def parse_trlc_file(self): 

122 assert self.cu.package is not None 

123 

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 

131 

132 ok = False 

133 

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() 

158 

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) 

164 

165 self.match_eof() 

166 

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) 

170 

171 return ok