1use pulldown_cmark::{Alignment, CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag, TagEnd};
7
8use crate::console::{ConsoleOptions, RenderResult, Renderable};
9use crate::rule::Rule;
10use crate::segment::Segment;
11use crate::style::Style;
12use crate::align::AlignMethod;
13use crate::table::{Cell, Column, Table};
14
15pub fn render_markdown(md: &str) -> MarkdownRender {
17 MarkdownRender {
18 source: md.to_string(),
19 width: None,
20 code_theme: "default".to_string(),
21 hyperlinks: true,
22 }
23}
24
25#[derive(Debug, Clone)]
27pub struct MarkdownRender {
28 source: String,
29 width: Option<usize>,
30 code_theme: String,
31 hyperlinks: bool,
32}
33
34impl MarkdownRender {
35 pub fn width(mut self, w: usize) -> Self { self.width = Some(w); self }
37
38 pub fn code_theme(mut self, theme: impl Into<String>) -> Self {
40 self.code_theme = theme.into();
41 self
42 }
43
44 pub fn hyperlinks(mut self, enabled: bool) -> Self {
46 self.hyperlinks = enabled;
47 self
48 }
49
50 fn get_style(name: &str) -> Style {
51 use crate::theme::default_theme;
52 let theme = default_theme();
53 theme.get(name).cloned().unwrap_or(Style::new())
54 }
55
56 fn code_style(&self) -> Style {
58 use crate::theme::default_theme;
59 let theme = default_theme();
60 let key = format!("markdown.code.{}", self.code_theme);
61 theme
62 .get(&key)
63 .cloned()
64 .unwrap_or_else(|| Self::get_style("markdown.code"))
65 }
66}
67
68impl Renderable for MarkdownRender {
69 fn render(&self, options: &ConsoleOptions) -> RenderResult {
70 let width = self.width.unwrap_or(options.max_width);
71 let parser = Parser::new_ext(&self.source, Options::all());
72
73 let mut lines: Vec<Vec<Segment>> = Vec::new();
74 let mut current_line: Vec<Segment> = Vec::new();
75 let mut in_code_block = false;
76 let mut heading_level = 0u8;
77 let mut list_depth = 0usize;
78 let mut current_link: Option<String> = None;
79 let mut link_text: Option<String> = None;
80 let mut in_table = false;
81 let mut table_alignments: Vec<Alignment> = Vec::new();
82 let mut table_rows: Vec<Vec<String>> = Vec::new();
83 let mut _table_is_header = false;
84 let mut current_row: Vec<String> = Vec::new();
85 let mut current_cell_text = String::new();
86
87 for event in parser {
88 match event {
89 Event::Start(Tag::Heading { level, .. }) => {
90 heading_level = level as u8;
91 let style = match level {
92 HeadingLevel::H1 => Self::get_style("markdown.h1"),
93 HeadingLevel::H2 => Self::get_style("markdown.h2"),
94 _ => Style::new().bold(true),
95 };
96 let prefix = "#".repeat(level as usize);
97 current_line.push(Segment::styled(
98 format!("{prefix} "),
99 style.clone(),
100 ));
101 }
102 Event::End(TagEnd::Heading(_)) => {
103 lines.push(current_line.clone());
104 current_line.clear();
105 if heading_level <= 2 {
107 let rule_char = if heading_level == 1 { '═' } else { '─' };
108 let rule_line = rule_char.to_string().repeat(width);
109 lines.push(vec![Segment::new(rule_line), Segment::line()]);
110 }
111 }
112 Event::Start(Tag::Paragraph) => {}
113 Event::End(TagEnd::Paragraph) => {
114 if !current_line.is_empty() {
115 current_line.push(Segment::line());
116 lines.push(current_line.clone());
117 current_line.clear();
118 }
119 lines.push(vec![Segment::line()]);
121 }
122 Event::Start(Tag::CodeBlock(kind)) => {
123 in_code_block = true;
124 let lang = match kind {
125 CodeBlockKind::Fenced(lang) => {
126 if lang.is_empty() { String::new() } else { lang.to_string() }
127 }
128 CodeBlockKind::Indented => String::new(),
129 };
130 let title = if lang.is_empty() {
131 "Code".to_string()
132 } else {
133 format!("Code: {lang}")
134 };
135 let code_style = self.code_style();
137 current_line.push(Segment::styled(
138 format!("┌─ {title} "),
139 code_style.clone(),
140 ));
141 current_line.push(Segment::line());
142 lines.push(current_line.clone());
143 current_line.clear();
144 }
145 Event::End(TagEnd::CodeBlock) => {
146 in_code_block = false;
147 if !current_line.is_empty() {
148 lines.push(current_line.clone());
149 current_line.clear();
150 }
151 let code_style = self.code_style();
152 lines.push(vec![Segment::styled(
153 format!("└{}", "─".repeat(width.saturating_sub(2))),
154 code_style,
155 ), Segment::line()]);
156 }
157 Event::Start(Tag::List(_)) => {
158 list_depth += 1;
159 }
160 Event::End(TagEnd::List(_)) => {
161 list_depth = list_depth.saturating_sub(1);
162 }
163 Event::Start(Tag::Item) => {
164 let indent = " ".repeat(list_depth.saturating_sub(1));
165 let bullet = if list_depth > 1 { "◦" } else { "•" };
166 current_line.push(Segment::new(format!("{indent}{bullet} ")));
167 }
168 Event::End(TagEnd::Item) => {
169 lines.push(current_line.clone());
170 current_line.clear();
171 }
172 Event::Start(Tag::BlockQuote) => {
173 let quote_style = Self::get_style("markdown.blockquote");
174 current_line.push(Segment::styled("▌ ", quote_style));
175 }
176 Event::End(TagEnd::BlockQuote) => {
177 lines.push(current_line.clone());
178 current_line.clear();
179 }
180 Event::Start(Tag::Emphasis) => {
181 current_line.push(Segment::styled("", Style::new().italic(true)));
182 }
183 Event::End(TagEnd::Emphasis) => {
184 }
186 Event::Start(Tag::Strong) => {
187 }
189 Event::End(TagEnd::Strong) => {}
190 Event::Start(Tag::Link { dest_url, .. }) => {
191 current_link = Some(dest_url.to_string());
192 link_text = Some(String::new());
193 }
194 Event::End(TagEnd::Link) => {
195 if let (Some(url), Some(text)) = (current_link.take(), link_text.take()) {
196 let link_style = Self::get_style("markdown.link");
197 let display = if text.is_empty() {
198 url.clone()
199 } else if self.hyperlinks {
200 format!("{text} ({url})")
201 } else {
202 text
203 };
204 current_line.push(Segment::styled(display, link_style));
205 }
206 }
207 Event::Text(text) | Event::Code(text) => {
208 let s: &str = &text;
209 if in_table {
210 current_cell_text.push_str(s);
211 if current_link.is_some() {
213 if let Some(ref mut lt) = link_text {
214 lt.push_str(s);
215 }
216 }
217 } else {
218 if current_link.is_some() {
220 if let Some(ref mut lt) = link_text {
221 lt.push_str(s);
222 }
223 }
224 if in_code_block {
225 for line in s.lines() {
227 current_line.push(Segment::new(format!("│ {line}")));
228 current_line.push(Segment::line());
229 lines.push(current_line.clone());
230 current_line.clear();
231 }
232 } else {
233 current_line.push(Segment::new(s));
234 }
235 }
236 }
237 Event::SoftBreak => {
238 current_line.push(Segment::new(" "));
239 }
240 Event::HardBreak => {
241 current_line.push(Segment::line());
242 lines.push(current_line.clone());
243 current_line.clear();
244 }
245 Event::Rule => {
246 let rule = Rule::new().characters("─");
247 let res = rule.render(options);
248 lines.extend(res.lines);
249 }
250 Event::Start(Tag::Table(alignments)) => {
251 in_table = true;
252 table_alignments = alignments;
253 table_rows = Vec::new();
254 }
255 Event::End(TagEnd::Table) => {
256 in_table = false;
257 if !table_rows.is_empty() {
258 let mut table = Table::new();
259 table.show_header = false;
260 table.show_edge = true;
261 for align in &table_alignments {
262 let justify = match align {
263 Alignment::Left => AlignMethod::Left,
264 Alignment::Right => AlignMethod::Right,
265 Alignment::Center => AlignMethod::Center,
266 Alignment::None => AlignMethod::Left,
267 };
268 table.add_column(Column::new("").justify(justify));
269 }
270 for (i, row) in table_rows.iter().enumerate() {
271 let cells: Vec<Cell> = row
272 .iter()
273 .enumerate()
274 .map(|(_, c)| {
275 if i == 0 {
276 Cell::new(c.clone()).style(Style::new().bold(true))
277 } else {
278 Cell::new(c.clone())
279 }
280 })
281 .collect();
282 table.add_row(cells);
283 }
284 let result = table.render(options);
285 lines.extend(result.lines);
286 }
287 }
288 Event::Start(Tag::TableHead) => {
289 _table_is_header = true;
290 }
291 Event::End(TagEnd::TableHead) => {
292 _table_is_header = false;
293 }
294 Event::Start(Tag::TableRow) => {
295 current_row = Vec::new();
296 }
297 Event::End(TagEnd::TableRow) => {
298 table_rows.push(current_row.clone());
299 current_row.clear();
300 }
301 Event::Start(Tag::Image { dest_url, title, .. }) => {
302 let image_style = Self::get_style("markdown.image");
304 let title_str = if title.is_empty() {
305 String::new()
306 } else {
307 format!(" \"{title}\"")
308 };
309 let image_text = format!("🖼 [Image: {dest_url}{title_str}]");
310 current_line.push(Segment::styled(image_text, image_style));
311 current_line.push(Segment::line());
312 lines.push(current_line.clone());
313 current_line.clear();
314 }
315 Event::End(TagEnd::Image) => {
316 }
318 Event::Start(Tag::TableCell) => {
319 current_cell_text = String::new();
320 }
321 Event::End(TagEnd::TableCell) => {
322 current_row.push(current_cell_text.clone());
323 current_cell_text.clear();
324 }
325 _ => {}
326 }
327 }
328
329 if !current_line.is_empty() {
331 current_line.push(Segment::line());
332 lines.push(current_line);
333 }
334
335 RenderResult { lines, items: Vec::new() }
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 #[test]
344 fn test_markdown_heading() {
345 let md = render_markdown("# Hello\n\nWorld");
346 let opts = ConsoleOptions::default();
347 let result = md.render(&opts);
348 let ansi = result.to_ansi();
349 assert!(ansi.contains("Hello"));
350 }
351}