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