Skip to main content

rustdown_cli/
markdown.rs

1use crate::render::MarkdownRenderer;
2use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser as MdParser, Tag};
3use std::io;
4use termcolor::{Color, ColorSpec, WriteColor};
5
6/// Strip ANSI escape sequences and other terminal control characters from a string
7/// to prevent terminal injection attacks via raw HTML in markdown.
8fn strip_terminal_escapes(s: &str) -> String {
9    let mut result = String::with_capacity(s.len());
10    let mut chars = s.chars();
11    while let Some(c) = chars.next() {
12        match c {
13            // ESC - skip the entire escape sequence
14            '\x1b' => {
15                // Consume the escape sequence
16                if let Some(next) = chars.next() {
17                    match next {
18                        // CSI sequence: ESC [ ... final_byte
19                        '[' => {
20                            for c in chars.by_ref() {
21                                if c.is_ascii_alphabetic() || c == '@' || c == '~' {
22                                    break;
23                                }
24                            }
25                        }
26                        // OSC sequence: ESC ] ... ST (BEL or ESC \)
27                        ']' => {
28                            let mut prev = ' ';
29                            for c in chars.by_ref() {
30                                if c == '\x07' || (c == '\\' && prev == '\x1b') {
31                                    break;
32                                }
33                                prev = c;
34                            }
35                        }
36                        // Other escape sequences (2-char): skip the next char
37                        _ => {}
38                    }
39                }
40            }
41            // Other C0 control characters (except common whitespace)
42            '\x00'..='\x08' | '\x0b' | '\x0c' | '\x0e'..='\x1a' | '\x7f' => {}
43            _ => result.push(c),
44        }
45    }
46    result
47}
48
49pub fn render_markdown(
50    md: &str,
51    preserve_fences: bool,
52    output: &mut impl WriteColor,
53) -> io::Result<()> {
54    let mut options = Options::empty();
55    options.insert(Options::ENABLE_STRIKETHROUGH);
56    options.insert(Options::ENABLE_TABLES);
57    options.insert(Options::ENABLE_FOOTNOTES);
58    options.insert(Options::ENABLE_TASKLISTS);
59
60    let parser = MdParser::new_ext(md, options);
61    let mut renderer = MarkdownRenderer::new(output, preserve_fences);
62
63    for event in parser {
64        match event {
65            Event::Start(tag) => match tag {
66                Tag::Paragraph => {
67                    // nothing to do at paragraph start
68                }
69                Tag::Heading { level, .. } => {
70                    let num = match level {
71                        HeadingLevel::H1 => 1,
72                        HeadingLevel::H2 => 2,
73                        HeadingLevel::H3 => 3,
74                        HeadingLevel::H4 => 4,
75                        HeadingLevel::H5 => 5,
76                        HeadingLevel::H6 => 6,
77                    };
78                    renderer.start_heading(num);
79                }
80                Tag::List(start_number) => {
81                    renderer.start_list(start_number)?;
82                }
83                Tag::Item => {
84                    renderer.render_list_item_start()?;
85                }
86                Tag::Emphasis => renderer.set_emphasis(true),
87                Tag::Strong => renderer.set_strong(true),
88                Tag::Strikethrough => renderer.set_strikethrough(true),
89                Tag::Link { dest_url, .. } => renderer.start_link(&dest_url),
90                Tag::Image { dest_url, .. } => renderer.start_image(&dest_url),
91                Tag::CodeBlock(kind) => {
92                    renderer.start_code_block(match kind {
93                        CodeBlockKind::Fenced(info) => pulldown_cmark::CodeBlockKind::Fenced(info),
94                        CodeBlockKind::Indented => pulldown_cmark::CodeBlockKind::Indented,
95                    })?;
96                }
97                Tag::BlockQuote(_) => {
98                    renderer.start_blockquote()?;
99                }
100                Tag::Table(_) => {
101                    renderer.start_table()?;
102                }
103                Tag::TableHead => renderer.start_table_head(),
104                Tag::TableRow => renderer.start_table_row(),
105                Tag::TableCell => renderer.start_table_cell(),
106                _ => {}
107            },
108            Event::End(tag_end) => match tag_end {
109                pulldown_cmark::TagEnd::TableCell => renderer.end_table_cell(),
110                pulldown_cmark::TagEnd::TableRow => renderer.end_table_row(),
111                pulldown_cmark::TagEnd::TableHead => renderer.end_table_head(),
112                pulldown_cmark::TagEnd::Table => renderer.end_table()?,
113                pulldown_cmark::TagEnd::Paragraph => renderer.end_paragraph()?,
114                pulldown_cmark::TagEnd::Heading(_) => renderer.end_heading()?,
115                pulldown_cmark::TagEnd::List(_) => renderer.end_list(),
116                pulldown_cmark::TagEnd::Item => renderer.end_item()?,
117                pulldown_cmark::TagEnd::Emphasis => renderer.set_emphasis(false),
118                pulldown_cmark::TagEnd::Strong => renderer.set_strong(false),
119                pulldown_cmark::TagEnd::Strikethrough => renderer.set_strikethrough(false),
120                pulldown_cmark::TagEnd::Link => renderer.end_link()?,
121                pulldown_cmark::TagEnd::Image => renderer.end_image()?,
122                pulldown_cmark::TagEnd::CodeBlock => renderer.end_code_block()?,
123                pulldown_cmark::TagEnd::BlockQuote(_) => renderer.end_blockquote(),
124                _ => {}
125            },
126            Event::Text(text) => renderer.write_event_text(&text)?,
127            Event::Code(code) => renderer.write_event_code(&code)?,
128            Event::Html(html) => {
129                let sanitized = strip_terminal_escapes(&html);
130                write!(renderer.output, "{}", sanitized)?;
131            }
132            Event::SoftBreak => renderer.soft_break()?,
133            Event::HardBreak => renderer.hard_break()?,
134            Event::Rule => renderer.render_rule()?,
135            Event::FootnoteReference(name) => {
136                let mut spec = ColorSpec::new();
137                spec.set_fg(Some(Color::Blue)).set_bold(true);
138                renderer.output.set_color(&spec)?;
139                write!(renderer.output, "[^{}]", name)?;
140                renderer.output.reset()?;
141            }
142            Event::TaskListMarker(checked) => renderer.render_task_list_item(checked)?,
143            _ => {}
144        }
145    }
146
147    renderer.flush()?;
148    Ok(())
149}