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