intelli_shell/utils/
markdown.rs1use crossterm::style::{Attribute, Color, ContentStyle, Stylize};
2use pulldown_cmark::{Event, HeadingLevel, Options, Parser, Tag, TagEnd};
3
4use crate::config::Theme;
5
6pub fn render_markdown_to_ansi(markdown: &str, theme: &Theme) -> String {
15 let parser = Parser::new_ext(markdown, Options::all());
16 let mut output = String::new();
17 let mut style_stack: Vec<ContentStyle> = vec![theme.primary];
18 let mut list_depth: usize = 0;
19 let mut code_block_buffer: Option<String> = None;
20
21 fn ensure_newlines(output: &mut String, count: usize) {
22 if output.is_empty() {
23 return;
24 }
25 let current = if output.ends_with("\n\n") {
26 2
27 } else if output.ends_with('\n') {
28 1
29 } else {
30 0
31 };
32 if current < count {
33 output.push_str(&"\n".repeat(count - current));
34 }
35 }
36
37 for event in parser {
38 match event {
39 Event::Start(tag) => match tag {
40 Tag::Heading { level, .. } => {
41 ensure_newlines(&mut output, 2);
42 let mut style = theme.comment;
43 style.attributes.set(Attribute::Bold);
44 if matches!(level, HeadingLevel::H3 | HeadingLevel::H4) {
45 style.attributes.set(Attribute::Underlined);
46 }
47 style_stack.push(style);
48 }
49 Tag::Paragraph if output.ends_with('\n') || output.is_empty() => {
50 ensure_newlines(&mut output, 2);
54 }
55 Tag::Emphasis => {
56 let mut style = style_stack.last().cloned().unwrap_or_default();
57 style.attributes.set(Attribute::Italic);
58 style_stack.push(style);
59 }
60 Tag::Strong => {
61 let mut style = style_stack.last().cloned().unwrap_or_default();
62 style.attributes.set(Attribute::Bold);
63 style_stack.push(style);
64 }
65 Tag::List(_) => {
66 list_depth += 1;
67 if list_depth == 1 {
68 ensure_newlines(&mut output, 2);
69 } else {
70 ensure_newlines(&mut output, 1);
71 }
72 }
73 Tag::Item => {
74 ensure_newlines(&mut output, 1);
75 let indent = " ".repeat(list_depth.saturating_sub(1) * 2);
76 let bullet = match list_depth {
77 1 => "• ",
78 2 => "◦ ",
79 _ => "▪ ",
80 };
81 output.push_str(" "); output.push_str(&indent);
83 output.push_str(bullet);
84 }
85 Tag::CodeBlock(kind) => {
86 ensure_newlines(&mut output, 2);
87 if let pulldown_cmark::CodeBlockKind::Fenced(lang) = kind
88 && !lang.is_empty()
89 {
90 let label_style = ContentStyle::default()
91 .with(Color::Black)
92 .on(Color::AnsiValue(244)) .bold();
94 output.push_str(&label_style.apply(&format!(" {} ", lang)).to_string());
95 output.push('\n');
96 }
97 let mut style = theme.secondary;
98 if style.background_color.is_none() {
99 style.background_color = theme.highlight;
100 }
101 style_stack.push(style);
102 code_block_buffer = Some(String::new());
103 }
104 Tag::Link { .. } => {
105 let mut style = style_stack.last().cloned().unwrap_or_default();
106 style.attributes.set(Attribute::Underlined);
107 style.foreground_color = Some(Color::Blue);
108 style_stack.push(style);
109 }
110 Tag::Strikethrough => {
111 let mut style = style_stack.last().cloned().unwrap_or_default();
112 style.attributes.set(Attribute::CrossedOut);
113 style_stack.push(style);
114 }
115 _ => {}
116 },
117 Event::End(tag) => match tag {
118 TagEnd::Heading { .. } => {
119 style_stack.pop();
120 ensure_newlines(&mut output, 1);
121 }
122 TagEnd::Paragraph => {
123 ensure_newlines(&mut output, 1);
124 }
125 TagEnd::Emphasis | TagEnd::Strong | TagEnd::Link | TagEnd::Strikethrough => {
126 style_stack.pop();
127 }
128 TagEnd::CodeBlock => {
129 let style = style_stack.pop().unwrap_or_default();
130 if let Some(code) = code_block_buffer.take() {
131 let lines: Vec<&str> = code.lines().collect();
132 let max_width = lines.iter().map(|l| l.len()).max().unwrap_or(0);
133 for line in lines {
134 let padded = format!(" {:<width$} ", line, width = max_width);
135 output.push_str(&style.apply(&padded).to_string());
136 output.push('\n');
137 }
138 }
139 ensure_newlines(&mut output, 1);
140 }
141 TagEnd::List(_) => {
142 list_depth = list_depth.saturating_sub(1);
143 ensure_newlines(&mut output, 1);
144 }
145 TagEnd::Item => {
146 ensure_newlines(&mut output, 1);
147 }
148 _ => {}
149 },
150 Event::Text(text) => {
151 if let Some(ref mut buffer) = code_block_buffer {
152 buffer.push_str(&text);
153 } else {
154 let style = style_stack.last().cloned().unwrap_or_default();
155 output.push_str(&style.apply(&text).to_string());
156 }
157 }
158 Event::Code(code) => {
159 let mut style = theme.accent;
160 if style.background_color.is_none() {
161 style.background_color = theme.highlight;
162 }
163 output.push_str(&style.apply(&code).to_string());
164 }
165 Event::TaskListMarker(checked) => {
166 if checked {
167 output.push_str("[x] ");
168 } else {
169 output.push_str("[ ] ");
170 }
171 }
172 Event::SoftBreak | Event::HardBreak => {
173 output.push('\n');
174 }
175 Event::Rule => {
176 ensure_newlines(&mut output, 1);
177 output.push_str(&theme.secondary.apply(&"─".repeat(60)).to_string());
178 output.push('\n');
179 }
180 _ => {}
181 }
182 }
183
184 output.trim_end().to_string()
185}