Skip to main content

rustdown_cli/render/
mod.rs

1mod table;
2
3use std::io;
4use termcolor::{Color, ColorSpec, WriteColor};
5
6pub(crate) struct MarkdownRenderer<'a, W: WriteColor + ?Sized> {
7    pub(crate) output: &'a mut W,
8    // State tracking
9    heading_level: Option<u32>,
10    heading_buf: String,
11    list_depth: usize,
12    list_item_number: Vec<usize>,
13    current_list_is_ordered: Vec<bool>,
14    // Track whether the last written character was whitespace/newline
15    last_was_space: bool,
16    in_emphasis: bool,
17    in_strong: bool,
18    in_strikethrough: bool,
19    in_code_block: bool,
20    in_blockquote: bool,
21    in_link: bool,
22    link_url: String,
23    in_image: bool,
24    image_alt: String,
25    image_url: String,
26    // buffer for code blocks
27    code_buf: String,
28    code_lang: Option<String>,
29    preserve_code_fences: bool,
30    // table rendering state
31    in_table: bool,
32    in_table_head: bool,
33    current_cell: String,
34    current_row: Vec<String>,
35    // collected table rows while inside a table
36    table_rows: Vec<Vec<String>>,
37    table_header_count: usize,
38}
39
40impl<'a, W: WriteColor + ?Sized> MarkdownRenderer<'a, W> {
41    pub(crate) fn new(output: &'a mut W, preserve_code_fences: bool) -> Self {
42        Self {
43            output,
44            heading_level: None,
45            heading_buf: String::new(),
46            list_depth: 0,
47            list_item_number: Vec::new(),
48            current_list_is_ordered: Vec::new(),
49            last_was_space: true,
50            in_emphasis: false,
51            in_strong: false,
52            in_strikethrough: false,
53            in_code_block: false,
54            in_blockquote: false,
55            in_link: false,
56            link_url: String::new(),
57            in_image: false,
58            image_alt: String::new(),
59            image_url: String::new(),
60            code_buf: String::new(),
61            code_lang: None,
62            preserve_code_fences,
63            in_table: false,
64            in_table_head: false,
65            current_cell: String::new(),
66            current_row: Vec::new(),
67            table_rows: Vec::new(),
68            table_header_count: 0,
69        }
70    }
71
72    pub(crate) fn flush(&mut self) -> io::Result<()> {
73        self.output.flush()
74    }
75
76    pub(crate) fn ensure_newline(&mut self) -> io::Result<()> {
77        if !self.last_was_space {
78            writeln!(self.output)?;
79            self.last_was_space = true;
80        }
81        Ok(())
82    }
83
84    pub(crate) fn set_emphasis(&mut self, on: bool) {
85        self.in_emphasis = on;
86    }
87
88    pub(crate) fn set_strong(&mut self, on: bool) {
89        self.in_strong = on;
90    }
91
92    pub(crate) fn set_strikethrough(&mut self, on: bool) {
93        self.in_strikethrough = on;
94    }
95
96    pub(crate) fn start_heading(&mut self, level: u32) {
97        self.heading_level = Some(level);
98        self.heading_buf.clear();
99    }
100
101    pub(crate) fn push_heading_softbreak(&mut self) {
102        self.heading_buf.push(' ');
103    }
104
105    pub(crate) fn end_heading(&mut self) -> io::Result<()> {
106        if let Some(level) = self.heading_level {
107            let heading_text = self.heading_buf.clone();
108            self.render_heading(level, &heading_text)?;
109        }
110        self.heading_level = None;
111        self.heading_buf.clear();
112        Ok(())
113    }
114
115    pub(crate) fn start_list(&mut self, start_number: Option<u64>) -> io::Result<()> {
116        self.ensure_newline()?;
117
118        let is_ordered = start_number.is_some();
119        self.current_list_is_ordered.push(is_ordered);
120        if is_ordered && self.list_item_number.len() <= self.list_depth {
121            self.list_item_number.push(0);
122        }
123        self.list_depth += 1;
124        Ok(())
125    }
126
127    pub(crate) fn end_list(&mut self) {
128        self.list_depth = self.list_depth.saturating_sub(1);
129        self.current_list_is_ordered.pop();
130        if self.current_list_is_ordered.is_empty() {
131            self.list_item_number.clear();
132        }
133        self.last_was_space = true;
134    }
135
136    pub(crate) fn end_item(&mut self) -> io::Result<()> {
137        writeln!(self.output)?;
138        self.last_was_space = true;
139        Ok(())
140    }
141
142    pub(crate) fn start_link(&mut self, dest_url: &str) {
143        self.in_link = true;
144        self.link_url = dest_url.to_string();
145    }
146
147    pub(crate) fn end_link(&mut self) -> io::Result<()> {
148        if !self.link_url.is_empty() {
149            let mut url_spec = ColorSpec::new();
150            url_spec.set_fg(Some(Color::Cyan));
151            self.output.set_color(&url_spec)?;
152            write!(self.output, " ({})", self.link_url)?;
153            self.output.reset()?;
154        }
155        self.in_link = false;
156        self.link_url.clear();
157        self.last_was_space = false;
158        Ok(())
159    }
160
161    pub(crate) fn start_image(&mut self, dest_url: &str) {
162        self.in_image = true;
163        self.image_url = dest_url.to_string();
164        self.image_alt.clear();
165    }
166
167    pub(crate) fn end_image(&mut self) -> io::Result<()> {
168        let image_url = self.image_url.clone();
169        let image_alt = self.image_alt.clone();
170        self.render_image(&image_url, &image_alt)?;
171        self.in_image = false;
172        self.image_url.clear();
173        self.image_alt.clear();
174        self.last_was_space = false;
175        Ok(())
176    }
177
178    pub(crate) fn start_code_block(
179        &mut self,
180        kind: pulldown_cmark::CodeBlockKind,
181    ) -> io::Result<()> {
182        self.in_code_block = true;
183        self.code_buf.clear();
184        self.code_lang = match kind {
185            pulldown_cmark::CodeBlockKind::Fenced(info) => {
186                let s = info.split_whitespace().next().unwrap_or("").to_string();
187                if s.is_empty() { None } else { Some(s) }
188            }
189            pulldown_cmark::CodeBlockKind::Indented => None,
190        };
191        self.ensure_newline()?;
192        Ok(())
193    }
194
195    pub(crate) fn end_code_block(&mut self) -> io::Result<()> {
196        let code = std::mem::take(&mut self.code_buf);
197        let lang_opt = self.code_lang.take();
198        self.render_code_block(&code, lang_opt.as_deref())?;
199        self.in_code_block = false;
200        Ok(())
201    }
202
203    pub(crate) fn start_blockquote(&mut self) -> io::Result<()> {
204        self.in_blockquote = true;
205        self.render_blockquote_start()
206    }
207
208    pub(crate) fn end_blockquote(&mut self) {
209        self.in_blockquote = false;
210    }
211
212    pub(crate) fn start_table(&mut self) -> io::Result<()> {
213        self.in_table = true;
214        self.in_table_head = false;
215        self.ensure_newline()?;
216        Ok(())
217    }
218
219    pub(crate) fn start_table_head(&mut self) {
220        self.in_table_head = true;
221        self.current_row.clear();
222    }
223
224    pub(crate) fn start_table_row(&mut self) {
225        if !self.in_table_head {
226            self.current_row.clear();
227        }
228    }
229
230    pub(crate) fn start_table_cell(&mut self) {
231        self.current_cell.clear();
232    }
233
234    pub(crate) fn end_table_cell(&mut self) {
235        self.current_row.push(self.current_cell.clone());
236        self.current_cell.clear();
237    }
238
239    pub(crate) fn end_table_row(&mut self) {
240        self.table_rows.push(self.current_row.clone());
241        self.current_row.clear();
242        self.last_was_space = true;
243    }
244
245    pub(crate) fn end_table_head(&mut self) {
246        if !self.current_row.is_empty() {
247            self.table_rows.push(self.current_row.clone());
248            self.current_row.clear();
249        }
250        self.table_header_count = self.table_rows.len();
251        self.in_table_head = false;
252    }
253
254    pub(crate) fn end_table(&mut self) -> io::Result<()> {
255        self.render_table()?;
256        self.table_rows.clear();
257        self.table_header_count = 0;
258        self.in_table = false;
259        writeln!(self.output)?;
260        self.last_was_space = true;
261        Ok(())
262    }
263
264    pub(crate) fn write_event_text(&mut self, text: &str) -> io::Result<()> {
265        if self.in_code_block {
266            self.code_buf.push_str(text);
267            return Ok(());
268        }
269        if self.in_table {
270            self.current_cell.push_str(text);
271            return Ok(());
272        }
273        self.write_text(text)
274    }
275
276    pub(crate) fn write_event_code(&mut self, code: &str) -> io::Result<()> {
277        if self.in_table {
278            self.current_cell.push('`');
279            self.current_cell.push_str(code);
280            self.current_cell.push('`');
281            return Ok(());
282        }
283        if !self.in_code_block {
284            self.render_inline_code(code)?;
285        }
286        Ok(())
287    }
288
289    pub(crate) fn soft_break(&mut self) -> io::Result<()> {
290        if self.heading_level.is_some() {
291            self.push_heading_softbreak();
292            Ok(())
293        } else if self.in_blockquote {
294            self.render_blockquote_start()
295        } else {
296            write!(self.output, " ")?;
297            self.last_was_space = true;
298            Ok(())
299        }
300    }
301
302    pub(crate) fn hard_break(&mut self) -> io::Result<()> {
303        writeln!(self.output)?;
304        self.last_was_space = true;
305        Ok(())
306    }
307
308    pub(crate) fn end_paragraph(&mut self) -> io::Result<()> {
309        if self.list_depth == 0 {
310            writeln!(self.output)?;
311            writeln!(self.output)?;
312        } else {
313            writeln!(self.output)?;
314        }
315        self.last_was_space = true;
316        Ok(())
317    }
318
319    // --- existing rendering routines below (unchanged) ---
320    pub(crate) fn render_heading(&mut self, level: u32, text: &str) -> io::Result<()> {
321        writeln!(self.output)?; // Add spacing before heading
322        match level {
323            1 => {
324                let mut spec = ColorSpec::new();
325                spec.set_fg(Some(Color::Cyan))
326                    .set_intense(true)
327                    .set_bold(true);
328                self.output.set_color(&spec)?;
329                writeln!(self.output, "{}", text)?;
330                self.output.reset()?;
331                writeln!(self.output, "{}", "=".repeat(text.chars().count()))?;
332            }
333            2 => {
334                let mut spec = ColorSpec::new();
335                spec.set_fg(Some(Color::Blue))
336                    .set_intense(true)
337                    .set_bold(true);
338                self.output.set_color(&spec)?;
339                writeln!(self.output, "{}", text)?;
340                self.output.reset()?;
341                writeln!(self.output, "{}", "-".repeat(text.chars().count()))?;
342            }
343            3 => {
344                let mut spec = ColorSpec::new();
345                spec.set_fg(Some(Color::Green)).set_bold(true);
346                self.output.set_color(&spec)?;
347                writeln!(self.output, "### {}", text)?;
348                self.output.reset()?;
349            }
350            4 => {
351                let mut spec = ColorSpec::new();
352                spec.set_fg(Some(Color::Yellow)).set_bold(true);
353                self.output.set_color(&spec)?;
354                writeln!(self.output, "#### {}", text)?;
355                self.output.reset()?;
356            }
357            5 => {
358                let mut spec = ColorSpec::new();
359                spec.set_fg(Some(Color::Magenta));
360                self.output.set_color(&spec)?;
361                writeln!(self.output, "##### {}", text)?;
362                self.output.reset()?;
363            }
364            6 => {
365                let mut spec = ColorSpec::new();
366                spec.set_fg(Some(Color::White)).set_intense(false);
367                self.output.set_color(&spec)?;
368                writeln!(self.output, "###### {}", text)?;
369                self.output.reset()?;
370            }
371            _ => {
372                writeln!(self.output, "{}", text)?;
373            }
374        }
375        writeln!(self.output)?; // Add spacing after heading
376        Ok(())
377    }
378
379    pub(crate) fn write_text(&mut self, text: &str) -> io::Result<()> {
380        if self.heading_level.is_some() {
381            self.heading_buf.push_str(text);
382            return Ok(());
383        }
384
385        if self.in_image {
386            self.image_alt.push_str(text);
387            return Ok(());
388        }
389
390        // Handle table cell text with formatting preservation
391        if self.in_table {
392            self.current_cell.push_str(text);
393            return Ok(());
394        }
395
396        let mut spec = ColorSpec::new();
397
398        if self.in_strong {
399            spec.set_bold(true);
400        }
401        if self.in_emphasis {
402            spec.set_italic(true);
403        }
404        if self.in_strikethrough {
405            spec.set_strikethrough(true);
406        }
407        if self.in_link {
408            spec.set_fg(Some(Color::Blue)).set_underline(true);
409        }
410        if self.in_blockquote {
411            spec.set_fg(Some(Color::Yellow));
412        }
413
414        self.output.set_color(&spec)?;
415        write!(self.output, "{}", text)?;
416        self.output.reset()?;
417
418        // Update trailing-space tracking
419        self.last_was_space = text
420            .chars()
421            .rev()
422            .next()
423            .map(|c| c.is_whitespace())
424            .unwrap_or(false);
425
426        Ok(())
427    }
428
429    pub(crate) fn render_list_item_start(&mut self) -> io::Result<()> {
430        let indent = "  ".repeat(self.list_depth.saturating_sub(1));
431
432        let is_ordered = self
433            .current_list_is_ordered
434            .get(self.list_depth.saturating_sub(1))
435            .copied()
436            .unwrap_or(false);
437
438        // Ensure previous content ended on its own line before the list marker
439        if !self.last_was_space {
440            writeln!(self.output)?;
441        }
442
443        if is_ordered {
444            if self.list_depth > self.list_item_number.len() {
445                self.list_item_number.push(1);
446            } else if self.list_depth > 0 {
447                self.list_item_number[self.list_depth - 1] += 1;
448            }
449            let num = self
450                .list_item_number
451                .get(self.list_depth.saturating_sub(1))
452                .copied()
453                .unwrap_or(1);
454            write!(self.output, "{}{}. ", indent, num)?;
455        } else {
456            // Improved CommonMark-compatible bullet selection
457            let bullet = match (self.list_depth.saturating_sub(1)) % 3 {
458                0 => "-", // Use dash for top level (more CommonMark compatible)
459                1 => "*", // Asterisk for second level
460                _ => "+", // Plus for third level and beyond
461            };
462            write!(self.output, "{}{} ", indent, bullet)?;
463        }
464        self.last_was_space = false;
465        Ok(())
466    }
467
468    pub(crate) fn render_inline_code(&mut self, code: &str) -> io::Result<()> {
469        let mut spec = ColorSpec::new();
470        spec.set_fg(Some(Color::Red));
471        self.output.set_color(&spec)?;
472        write!(self.output, "`{}`", code)?;
473        self.output.reset()?;
474        self.last_was_space = false;
475        Ok(())
476    }
477
478    pub(crate) fn render_image(&mut self, url: &str, alt: &str) -> io::Result<()> {
479        let mut spec = ColorSpec::new();
480        spec.set_fg(Some(Color::Magenta));
481        self.output.set_color(&spec)?;
482        write!(self.output, "🖼️  [IMAGE: {}]", alt)?;
483        self.output.reset()?;
484
485        let mut url_spec = ColorSpec::new();
486        url_spec.set_fg(Some(Color::Cyan));
487        self.output.set_color(&url_spec)?;
488        write!(self.output, " ({})", url)?;
489        self.output.reset()?;
490        self.last_was_space = false;
491        Ok(())
492    }
493
494    pub(crate) fn render_blockquote_start(&mut self) -> io::Result<()> {
495        let mut spec = ColorSpec::new();
496        spec.set_fg(Some(Color::Yellow));
497        self.output.set_color(&spec)?;
498        // ensure quote starts on a new line
499        if !self.last_was_space {
500            writeln!(self.output)?;
501        }
502        write!(self.output, "│ ")?;
503        self.output.reset()?;
504        self.last_was_space = false;
505        Ok(())
506    }
507
508    pub(crate) fn render_rule(&mut self) -> io::Result<()> {
509        let mut spec = ColorSpec::new();
510        spec.set_fg(Some(Color::White)).set_intense(false);
511        self.output.set_color(&spec)?;
512        writeln!(self.output, "{}", "─".repeat(60))?;
513        self.output.reset()?;
514        self.last_was_space = true;
515        Ok(())
516    }
517
518    pub(crate) fn render_task_list_item(&mut self, checked: bool) -> io::Result<()> {
519        let indent = "  ".repeat(self.list_depth.saturating_sub(1));
520        let checkbox = if checked { "☑" } else { "☐" };
521        let mut spec = ColorSpec::new();
522        spec.set_fg(Some(if checked { Color::Green } else { Color::White }));
523        self.output.set_color(&spec)?;
524        write!(self.output, "{}{} ", indent, checkbox)?;
525        self.output.reset()?;
526        Ok(())
527    }
528
529    pub(crate) fn render_code_block(&mut self, code: &str, lang: Option<&str>) -> io::Result<()> {
530        writeln!(self.output)?;
531
532        if self.preserve_code_fences {
533            // Show original fenced block with backticks
534            let mut spec = ColorSpec::new();
535            spec.set_fg(Some(Color::Green)).set_bold(true);
536            self.output.set_color(&spec)?;
537            writeln!(self.output, "```{}", lang.unwrap_or(""))?;
538            self.output.reset()?;
539
540            // Print code without additional indentation
541            for line in code.lines() {
542                writeln!(self.output, "{}", line)?;
543            }
544
545            self.output.set_color(&spec)?;
546            writeln!(self.output, "```")?;
547            self.output.reset()?;
548        } else {
549            // Original rendering with language label and indentation
550            if let Some(l) = lang {
551                let mut spec = ColorSpec::new();
552                spec.set_fg(Some(Color::Green)).set_bold(true);
553                self.output.set_color(&spec)?;
554                writeln!(self.output, "[{}]", l)?;
555                self.output.reset()?;
556            }
557            for line in code.lines() {
558                write!(self.output, "    {}\n", line)?;
559            }
560        }
561
562        writeln!(self.output)?;
563        self.last_was_space = true;
564        Ok(())
565    }
566
567    pub(crate) fn render_table(&mut self) -> io::Result<()> {
568        table::render_table(self)
569    }
570}