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 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 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 code_buf: String,
28 code_lang: Option<String>,
29 preserve_code_fences: bool,
30 in_table: bool,
32 in_table_head: bool,
33 current_cell: String,
34 current_row: Vec<String>,
35 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 pub(crate) fn render_heading(&mut self, level: u32, text: &str) -> io::Result<()> {
321 writeln!(self.output)?; 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)?; 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 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 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 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 let bullet = match (self.list_depth.saturating_sub(1)) % 3 {
458 0 => "-", 1 => "*", _ => "+", };
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 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 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 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 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}