Coverage for src / osiris_cli / markdown_renderer.py: 0%

80 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-27 17:41 +0200

1#!/usr/bin/env python3 

2""" 

3Clean markdown renderer for Osiris CLI 

4Converts markdown to beautiful terminal output WITHOUT escape code pollution 

5""" 

6 

7from rich.console import Console 

8from rich.text import Text 

9import re 

10 

11console = Console() 

12 

13 

14class CleanMarkdownRenderer: 

15 """ 

16 Clean markdown renderer that outputs directly without escape codes 

17 """ 

18 

19 COLORS = { 

20 "h1": "bold bright_cyan", 

21 "h2": "bold cyan", 

22 "h3": "bold blue", 

23 "h4": "bold", 

24 "bold": "bold white", 

25 "italic": "italic", 

26 "code": "yellow on grey11", 

27 "link": "blue underline", 

28 } 

29 

30 def __init__(self, console_instance=None): 

31 self.console = console_instance or Console() 

32 self.buffer = "" 

33 

34 def process_line(self, line: str) -> Text: 

35 """Process a single line of markdown and return styled Text""" 

36 text = Text() 

37 

38 # Handle headings 

39 if line.startswith('#### '): 

40 text.append(line[5:].strip(), style=self.COLORS["h4"]) 

41 return text 

42 elif line.startswith('### '): 

43 text.append(line[4:].strip(), style=self.COLORS["h3"]) 

44 return text 

45 elif line.startswith('## '): 

46 text.append(line[3:].strip(), style=self.COLORS["h2"]) 

47 return text 

48 elif line.startswith('# '): 

49 text.append(line[2:].strip(), style=self.COLORS["h1"]) 

50 return text 

51 

52 # Handle lists 

53 if line.strip().startswith(('- ', '* ', '+ ')): 

54 text.append(' • ', style="cyan") 

55 line = line.strip()[2:] 

56 elif re.match(r'^\d+\.\s', line.strip()): 

57 match = re.match(r'^(\d+)\.\s', line.strip()) 

58 text.append(f' {match.group(1)}. ', style="cyan") 

59 line = line.strip()[len(match.group(0)):] 

60 

61 # Process inline formatting 

62 remaining = line 

63 while remaining: 

64 # Bold 

65 bold_match = re.search(r'\*\*(.+?)\*\*', remaining) 

66 # Italic 

67 italic_match = re.search(r'\*(.+?)\*', remaining) 

68 # Code 

69 code_match = re.search(r'`(.+?)`', remaining) 

70 

71 # Find earliest match 

72 matches = [] 

73 if bold_match: 

74 matches.append(('bold', bold_match.start(), bold_match.end(), bold_match.group(1))) 

75 if italic_match and (not bold_match or italic_match.start() < bold_match.start()): 

76 matches.append(('italic', italic_match.start(), italic_match.end(), italic_match.group(1))) 

77 if code_match: 

78 matches.append(('code', code_match.start(), code_match.end(), code_match.group(1))) 

79 

80 if not matches: 

81 text.append(remaining) 

82 break 

83 

84 # Sort by position 

85 matches.sort(key=lambda x: x[1]) 

86 match_type, start, end, content = matches[0] 

87 

88 # Add text before match 

89 if start > 0: 

90 text.append(remaining[:start]) 

91 

92 # Add styled content 

93 if match_type == 'bold': 

94 text.append(content, style=self.COLORS["bold"]) 

95 elif match_type == 'italic': 

96 text.append(content, style=self.COLORS["italic"]) 

97 elif match_type == 'code': 

98 text.append(content, style=self.COLORS["code"]) 

99 

100 # Continue with remaining text 

101 remaining = remaining[end:] 

102 

103 return text 

104 

105 def render_chunk(self, chunk: str) -> bool: 

106 """ 

107 Process a chunk of text 

108 Returns True if content was rendered, False if buffering 

109 """ 

110 self.buffer += chunk 

111 

112 # Process complete lines only 

113 rendered_any = False 

114 while '\n' in self.buffer: 

115 line, self.buffer = self.buffer.split('\n', 1) 

116 if line.strip(): 

117 styled_text = self.process_line(line) 

118 self.console.print(styled_text) 

119 rendered_any = True 

120 else: 

121 self.console.print() 

122 rendered_any = True 

123 

124 return rendered_any 

125 

126 def flush(self): 

127 """Render any remaining content""" 

128 if self.buffer.strip(): 

129 styled_text = self.process_line(self.buffer) 

130 self.console.print(styled_text) 

131 self.buffer = "" 

132 

133 

134# Quick test 

135if __name__ == "__main__": 

136 renderer = CleanMarkdownRenderer() 

137 

138 test_text = """## Test Heading 

139This is **bold** and this is *italic* and `code`. 

140 

141### Subheading 

142- Item 1 

143- Item 2 

144 

145#### Smaller heading 

146Some normal text here.""" 

147 

148 for line in test_text.split('\n'): 

149 renderer.render_chunk(line + '\n') 

150 renderer.flush() 

151