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

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 # The H1 heading takes the role of the 'package' keyword, so the 

34 # inherited preamble parsing applies unchanged to markdown files. 

35 PACKAGE_KEYWORD = "#" 

36 

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" 

40 

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 

62 

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) 

67 

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) 

81 

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

89 

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

95 

96 def parse_trlc_file(self): 

97 assert self.cu.package is not None 

98 

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 

106 

107 ok = False 

108 

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

134 

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) 

140 

141 self.match_eof() 

142 

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) 

146 

147 return ok