1use crate::render::MarkdownRenderer;
2use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser as MdParser, Tag};
3use std::io;
4use termcolor::{Color, ColorSpec, WriteColor};
5
6fn 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 '\x1b' => {
15 if let Some(next) = chars.next() {
17 match next {
18 '[' => {
20 for c in chars.by_ref() {
21 if c.is_ascii_alphabetic() || c == '@' || c == '~' {
22 break;
23 }
24 }
25 }
26 ']' => {
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 _ => {}
38 }
39 }
40 }
41 '\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 }
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}