1use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd, CodeBlockKind, HeadingLevel};
7use ratatui::style::{Color, Modifier, Style};
8use ratatui::text::{Line, Span};
9
10use super::mermaid::MermaidBlock;
11
12#[derive(Debug)]
14pub enum PreviewBlock {
15 Text(Vec<Line<'static>>),
17 Mermaid(MermaidBlock),
20}
21
22pub fn parse_markdown(source: &str) -> Vec<PreviewBlock> {
24 let mut opts = Options::empty();
25 opts.insert(Options::ENABLE_TABLES);
26 opts.insert(Options::ENABLE_STRIKETHROUGH);
27 opts.insert(Options::ENABLE_TASKLISTS);
28
29 let parser = Parser::new_ext(source, opts);
30 let events: Vec<Event> = parser.collect();
31
32 let mut blocks: Vec<PreviewBlock> = Vec::new();
33 let mut lines: Vec<Line<'static>> = Vec::new();
34 let mut renderer = MarkdownRenderer::new();
35
36 let mut i = 0;
37 while i < events.len() {
38 match &events[i] {
39 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang)))
40 if lang.as_ref() == "mermaid" =>
41 {
42 if !lines.is_empty() {
44 blocks.push(PreviewBlock::Text(std::mem::take(&mut lines)));
45 }
46 let mut mermaid_src = String::new();
48 i += 1;
49 while i < events.len() {
50 match &events[i] {
51 Event::Text(text) => mermaid_src.push_str(text.as_ref()),
52 Event::End(TagEnd::CodeBlock) => break,
53 _ => {}
54 }
55 i += 1;
56 }
57 blocks.push(PreviewBlock::Mermaid(MermaidBlock::new(mermaid_src)));
58 i += 1;
59 continue;
60 }
61 _ => {
62 let new_lines = renderer.render_event(&events, i);
63 lines.extend(new_lines);
64 }
65 }
66 i += 1;
67 }
68
69 if !lines.is_empty() {
70 blocks.push(PreviewBlock::Text(lines));
71 }
72
73 blocks
74}
75
76struct MarkdownRenderer {
78 style_stack: Vec<Style>,
80 current_spans: Vec<Span<'static>>,
82 heading_level: Option<HeadingLevel>,
84 list_stack: Vec<(bool, usize)>,
86 in_blockquote: bool,
88 table_state: Option<TableState>,
90 in_code_block: bool,
92 code_block_lang: String,
93}
94
95struct TableState {
96 rows: Vec<Vec<String>>,
97 current_row: Vec<String>,
98 current_cell: String,
99 in_head: bool,
100}
101
102impl MarkdownRenderer {
103 fn new() -> Self {
104 Self {
105 style_stack: vec![Style::default()],
106 current_spans: Vec::new(),
107 heading_level: None,
108 list_stack: Vec::new(),
109 in_blockquote: false,
110 table_state: None,
111 in_code_block: false,
112 code_block_lang: String::new(),
113 }
114 }
115
116 fn current_style(&self) -> Style {
117 self.style_stack.last().copied().unwrap_or_default()
118 }
119
120 fn push_style(&mut self, modifier: Modifier, fg: Option<Color>) {
121 let mut style = self.current_style().add_modifier(modifier);
122 if let Some(color) = fg {
123 style = style.fg(color);
124 }
125 self.style_stack.push(style);
126 }
127
128 fn pop_style(&mut self) {
129 if self.style_stack.len() > 1 {
130 self.style_stack.pop();
131 }
132 }
133
134 fn flush_line(&mut self) -> Option<Line<'static>> {
135 if self.current_spans.is_empty() {
136 return None;
137 }
138 let spans = std::mem::take(&mut self.current_spans);
139
140 if self.in_blockquote {
142 let mut prefixed = vec![Span::styled(
143 " > ".to_string(),
144 Style::default().fg(Color::DarkGray).add_modifier(Modifier::DIM),
145 )];
146 prefixed.extend(spans);
147 Some(Line::from(prefixed))
148 } else {
149 Some(Line::from(spans))
150 }
151 }
152
153 fn render_event(&mut self, events: &[Event], idx: usize) -> Vec<Line<'static>> {
154 let mut lines = Vec::new();
155 let event = &events[idx];
156
157 match event {
158 Event::Start(Tag::Heading { level, .. }) => {
160 self.heading_level = Some(*level);
161 let (prefix, color) = match level {
162 HeadingLevel::H1 => ("# ", Color::Magenta),
163 HeadingLevel::H2 => ("## ", Color::Cyan),
164 HeadingLevel::H3 => ("### ", Color::Green),
165 HeadingLevel::H4 => ("#### ", Color::Yellow),
166 HeadingLevel::H5 => ("##### ", Color::Blue),
167 HeadingLevel::H6 => ("###### ", Color::Red),
168 };
169 self.push_style(Modifier::BOLD, Some(color));
170 self.current_spans.push(Span::styled(
171 prefix.to_string(),
172 self.current_style(),
173 ));
174 }
175 Event::End(TagEnd::Heading(_)) => {
176 if let Some(line) = self.flush_line() {
177 lines.push(line);
178 }
179 self.heading_level = None;
180 self.pop_style();
181 lines.push(Line::raw("")); }
183
184 Event::Start(Tag::Paragraph) => {}
185 Event::End(TagEnd::Paragraph) => {
186 if let Some(line) = self.flush_line() {
187 lines.push(line);
188 }
189 lines.push(Line::raw("")); }
191
192 Event::Start(Tag::Strong) => {
194 self.push_style(Modifier::BOLD, None);
195 }
196 Event::End(TagEnd::Strong) => {
197 self.pop_style();
198 }
199 Event::Start(Tag::Emphasis) => {
200 self.push_style(Modifier::ITALIC, None);
201 }
202 Event::End(TagEnd::Emphasis) => {
203 self.pop_style();
204 }
205 Event::Start(Tag::Strikethrough) => {
206 self.push_style(Modifier::CROSSED_OUT, None);
207 }
208 Event::End(TagEnd::Strikethrough) => {
209 self.pop_style();
210 }
211
212 Event::Code(code) => {
214 self.current_spans.push(Span::styled(
215 format!("`{code}`"),
216 Style::default()
217 .fg(Color::Yellow)
218 .add_modifier(Modifier::BOLD),
219 ));
220 }
221
222 Event::Text(text) => {
224 if self.in_code_block {
225 for line_text in text.as_ref().split('\n') {
227 if !self.current_spans.is_empty() {
228 if let Some(line) = self.flush_line() {
229 lines.push(line);
230 }
231 }
232 self.current_spans.push(Span::styled(
233 format!(" {line_text}"),
234 Style::default().fg(Color::Green),
235 ));
236 }
237 } else if let Some(ref mut table) = self.table_state {
238 table.current_cell.push_str(text.as_ref());
239 } else {
240 self.current_spans.push(Span::styled(
241 text.to_string(),
242 self.current_style(),
243 ));
244 }
245 }
246
247 Event::SoftBreak => {
248 self.current_spans.push(Span::raw(" ".to_string()));
249 }
250 Event::HardBreak => {
251 if let Some(line) = self.flush_line() {
252 lines.push(line);
253 }
254 }
255
256 Event::Start(Tag::Link { dest_url, .. }) => {
258 self.push_style(Modifier::UNDERLINED, Some(Color::Blue));
259 self.current_spans.push(Span::raw(String::new())); let _ = dest_url; }
263 Event::End(TagEnd::Link) => {
264 self.pop_style();
265 }
266
267 Event::Start(Tag::List(start_num)) => {
269 let ordered = start_num.is_some();
270 let start = start_num.unwrap_or(0) as usize;
271 self.list_stack.push((ordered, start));
272 }
273 Event::End(TagEnd::List(_)) => {
274 self.list_stack.pop();
275 if self.list_stack.is_empty() {
276 lines.push(Line::raw("")); }
278 }
279 Event::Start(Tag::Item) => {
280 let indent = " ".repeat(self.list_stack.len().saturating_sub(1));
281 if let Some((ordered, num)) = self.list_stack.last_mut() {
282 let bullet = if *ordered {
283 *num += 1;
284 format!("{indent}{}. ", *num)
285 } else {
286 format!("{indent} - ")
287 };
288 self.current_spans.push(Span::styled(
289 bullet,
290 Style::default().fg(Color::Cyan),
291 ));
292 }
293 }
294 Event::End(TagEnd::Item) => {
295 if let Some(line) = self.flush_line() {
296 lines.push(line);
297 }
298 }
299
300 Event::Start(Tag::BlockQuote(_)) => {
302 self.in_blockquote = true;
303 self.push_style(Modifier::DIM, Some(Color::DarkGray));
304 }
305 Event::End(TagEnd::BlockQuote(_)) => {
306 if let Some(line) = self.flush_line() {
307 lines.push(line);
308 }
309 self.in_blockquote = false;
310 self.pop_style();
311 lines.push(Line::raw(""));
312 }
313
314 Event::Start(Tag::CodeBlock(kind)) => {
316 self.in_code_block = true;
317 match kind {
318 CodeBlockKind::Fenced(lang) => {
319 self.code_block_lang = lang.to_string();
320 lines.push(Line::from(Span::styled(
321 format!(" ```{lang}"),
322 Style::default().fg(Color::DarkGray),
323 )));
324 }
325 CodeBlockKind::Indented => {
326 lines.push(Line::from(Span::styled(
327 " ```".to_string(),
328 Style::default().fg(Color::DarkGray),
329 )));
330 }
331 }
332 }
333 Event::End(TagEnd::CodeBlock) => {
334 if let Some(line) = self.flush_line() {
335 lines.push(line);
336 }
337 self.in_code_block = false;
338 self.code_block_lang.clear();
339 lines.push(Line::from(Span::styled(
340 " ```".to_string(),
341 Style::default().fg(Color::DarkGray),
342 )));
343 lines.push(Line::raw(""));
344 }
345
346 Event::Start(Tag::Table(_)) => {
348 self.table_state = Some(TableState {
349 rows: Vec::new(),
350 current_row: Vec::new(),
351 current_cell: String::new(),
352 in_head: false,
353 });
354 }
355 Event::End(TagEnd::Table) => {
356 if let Some(table) = self.table_state.take() {
357 lines.extend(render_table(&table.rows));
358 lines.push(Line::raw(""));
359 }
360 }
361 Event::Start(Tag::TableHead) => {
362 if let Some(ref mut t) = self.table_state {
363 t.in_head = true;
364 }
365 }
366 Event::End(TagEnd::TableHead) => {
367 if let Some(ref mut t) = self.table_state {
368 t.rows.push(std::mem::take(&mut t.current_row));
369 t.in_head = false;
370 }
371 }
372 Event::Start(Tag::TableRow) => {}
373 Event::End(TagEnd::TableRow) => {
374 if let Some(ref mut t) = self.table_state {
375 t.rows.push(std::mem::take(&mut t.current_row));
376 }
377 }
378 Event::Start(Tag::TableCell) => {
379 if let Some(ref mut t) = self.table_state {
380 t.current_cell.clear();
381 }
382 }
383 Event::End(TagEnd::TableCell) => {
384 if let Some(ref mut t) = self.table_state {
385 t.current_row.push(std::mem::take(&mut t.current_cell));
386 }
387 }
388
389 Event::Rule => {
391 lines.push(Line::from(Span::styled(
392 "──────────────────────────────────────────".to_string(),
393 Style::default().fg(Color::DarkGray),
394 )));
395 lines.push(Line::raw(""));
396 }
397
398 Event::TaskListMarker(checked) => {
400 let marker = if *checked { "[x] " } else { "[ ] " };
401 if let Some(last) = self.current_spans.last_mut() {
403 let content = last.content.to_string();
404 *last = Span::styled(
405 format!("{content}{marker}"),
406 Style::default().fg(if *checked { Color::Green } else { Color::Yellow }),
407 );
408 }
409 }
410
411 _ => {}
412 }
413
414 lines
415 }
416}
417
418fn render_table(rows: &[Vec<String>]) -> Vec<Line<'static>> {
420 if rows.is_empty() {
421 return Vec::new();
422 }
423
424 let num_cols = rows.iter().map(|r| r.len()).max().unwrap_or(0);
426 let mut col_widths = vec![0usize; num_cols];
427 for row in rows {
428 for (i, cell) in row.iter().enumerate() {
429 if i < num_cols {
430 col_widths[i] = col_widths[i].max(cell.len());
431 }
432 }
433 }
434
435 let mut lines = Vec::new();
436 let header_style = Style::default()
437 .fg(Color::Cyan)
438 .add_modifier(Modifier::BOLD);
439 let cell_style = Style::default();
440 let border_style = Style::default().fg(Color::DarkGray);
441
442 let top_border: String = col_widths
444 .iter()
445 .map(|w| "─".repeat(w + 2))
446 .collect::<Vec<_>>()
447 .join("┬");
448 lines.push(Line::from(Span::styled(
449 format!(" ┌{top_border}┐"),
450 border_style,
451 )));
452
453 for (ri, row) in rows.iter().enumerate() {
454 let is_header = ri == 0;
455 let style = if is_header { header_style } else { cell_style };
456
457 let mut spans = vec![Span::styled(" │".to_string(), border_style)];
458 for (ci, width) in col_widths.iter().enumerate() {
459 let cell = row.get(ci).map(|s| s.as_str()).unwrap_or("");
460 spans.push(Span::styled(format!(" {cell:<width$} ", width = width), style));
461 spans.push(Span::styled("│".to_string(), border_style));
462 }
463 lines.push(Line::from(spans));
464
465 if is_header {
467 let sep: String = col_widths
468 .iter()
469 .map(|w| "─".repeat(w + 2))
470 .collect::<Vec<_>>()
471 .join("┼");
472 lines.push(Line::from(Span::styled(
473 format!(" ├{sep}┤"),
474 border_style,
475 )));
476 }
477 }
478
479 let bot_border: String = col_widths
481 .iter()
482 .map(|w| "─".repeat(w + 2))
483 .collect::<Vec<_>>()
484 .join("┴");
485 lines.push(Line::from(Span::styled(
486 format!(" └{bot_border}┘"),
487 border_style,
488 )));
489
490 lines
491}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496
497 #[test]
498 fn test_heading_parsing() {
499 let blocks = parse_markdown("# Hello\n\nSome text");
500 assert!(!blocks.is_empty());
501 }
502
503 #[test]
504 fn test_mermaid_extraction() {
505 let md = "# Diagram\n\n```mermaid\ngraph TD\n A-->B\n```\n\nAfter.";
506 let blocks = parse_markdown(md);
507 let has_mermaid = blocks.iter().any(|b| matches!(b, PreviewBlock::Mermaid(_)));
508 assert!(has_mermaid, "Should extract mermaid block");
509 }
510
511 #[test]
512 fn test_table_rendering() {
513 let md = "| A | B |\n|---|---|\n| 1 | 2 |\n| 3 | 4 |";
514 let blocks = parse_markdown(md);
515 assert!(!blocks.is_empty());
516 }
517}