1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
use pulldown_cmark::{Parser, Event, Tag};
use ratatui::style::{Color, Style};
use std::path::Path;
use std::fs;
/// Represents a styled text segment for rendering markdown
pub struct StyledText {
pub text: String,
pub style: Style,
}
/// Renderer for Markdown content
pub struct MarkdownRenderer {
pub styled_lines: Vec<Vec<StyledText>>,
}
impl MarkdownRenderer {
/// Create a new markdown renderer from a file path
pub fn from_file<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
let content = fs::read_to_string(path)?;
Ok(Self::from_string(&content))
}
/// Create a new markdown renderer from a string
pub fn from_string(markdown: &str) -> Self {
let mut renderer = Self {
styled_lines: Vec::new(),
};
let parser = Parser::new(markdown);
renderer.process_events(parser);
renderer
}
fn process_events<'a, I>(&mut self, parser: I)
where
I: Iterator<Item = Event<'a>>,
{
let mut current_line = Vec::new();
let mut current_style = Style::default().fg(Color::White);
let mut in_header = false;
let mut in_bold = false;
let mut in_italic = false;
for event in parser {
match event {
Event::Start(tag) => match tag {
Tag::Heading(level, ..) => {
// Always ensure headers start on a new line
if !current_line.is_empty() {
self.styled_lines.push(current_line);
current_line = Vec::new();
}
// Add an extra empty line before headers (except at the very beginning)
if !self.styled_lines.is_empty() {
self.styled_lines.push(Vec::new());
}
in_header = true;
// Different header levels get different colors
current_style = match level {
pulldown_cmark::HeadingLevel::H1 => Style::default().fg(Color::Red),
pulldown_cmark::HeadingLevel::H2 => Style::default().fg(Color::Yellow),
_ => Style::default().fg(Color::Green),
};
},
Tag::Strong => {
in_bold = true;
// Bold text gets a brighter color
current_style = current_style.fg(Color::White);
},
Tag::Emphasis => {
in_italic = true;
// Italic text gets a different color
current_style = current_style.fg(Color::Cyan);
},
Tag::Paragraph => {
// Start a new paragraph
if !current_line.is_empty() {
self.styled_lines.push(current_line);
current_line = Vec::new();
}
},
_ => {}
},
Event::End(tag) => match tag {
Tag::Heading(..) => {
in_header = false;
self.styled_lines.push(current_line);
current_line = Vec::new();
// Add an extra empty line after headers
self.styled_lines.push(Vec::new());
current_style = Style::default().fg(Color::White);
},
Tag::Strong => {
in_bold = false;
// Reset style based on context
if in_header {
current_style = Style::default().fg(Color::Red);
} else if in_italic {
current_style = Style::default().fg(Color::Cyan);
} else {
current_style = Style::default().fg(Color::White);
}
},
Tag::Emphasis => {
in_italic = false;
// Reset style based on context
if in_header {
current_style = Style::default().fg(Color::Red);
} else if in_bold {
current_style = Style::default().fg(Color::White);
} else {
current_style = Style::default().fg(Color::White);
}
},
Tag::Paragraph => {
// End paragraph with empty line
self.styled_lines.push(Vec::new());
},
_ => {}
},
Event::Text(text) => {
current_line.push(StyledText {
text: text.to_string(),
style: current_style,
});
},
Event::SoftBreak => {
self.styled_lines.push(current_line);
current_line = Vec::new();
},
Event::HardBreak => {
self.styled_lines.push(current_line);
current_line = Vec::new();
},
_ => {}
}
}
// Add any remaining content
if !current_line.is_empty() {
self.styled_lines.push(current_line);
}
}
}