1use crate::console::ANSI_REGEX_PATTERN;
3use crossterm::{
4 queue,
5 style::{
6 Attribute, Color, Print, ResetColor, SetAttribute, SetBackgroundColor, SetForegroundColor,
7 },
8};
9use regex::Regex;
10use std::fmt::Write as _;
11use std::io::{self, Write};
12use std::sync::LazyLock;
13use syntect::easy::HighlightLines;
14use syntect::highlighting::{Theme, ThemeSet};
15use syntect::parsing::SyntaxSet;
16use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
17
18static SYNTAX_SET: LazyLock<SyntaxSet> = LazyLock::new(two_face::syntax::extra_no_newlines);
20static THEME: LazyLock<Theme> = LazyLock::new(|| {
21 let ts = ThemeSet::load_defaults();
22 ts.themes
23 .get("base16-ocean.dark")
24 .or_else(|| ts.themes.values().next())
25 .expect("No themes found")
26 .clone()
27});
28
29static RE_CODE_FENCE: LazyLock<Regex> =
31 LazyLock::new(|| Regex::new(r"^(\s*)([`~]{3,})(.*)$").unwrap());
32static RE_HEADER: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(#{1,6})\s+(.*)").unwrap());
33static RE_HR: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\s*[-*_]){3,}\s*$").unwrap());
34static RE_LIST: LazyLock<Regex> =
35 LazyLock::new(|| Regex::new(r"^(\s*)([-*+]|\d+\.)(?:(\s+)(.*)|$)").unwrap());
36static RE_BLOCKQUOTE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\s*>\s?)(.*)").unwrap());
37
38static RE_TABLE_ROW: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\s*\|(.*)\|\s*$").unwrap());
39static RE_TABLE_SEP: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[\s\|\-\:]+$").unwrap());
40
41static RE_MATH_BLOCK: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\s*\$\$\s*$").unwrap());
42
43const STYLE_H1: &str = "\x1b[1m";
45const STYLE_H2: &str = "\x1b[1m\x1b[94m";
46const STYLE_H3: &str = "\x1b[1m\x1b[36m";
47const STYLE_H_DEFAULT: &str = "\x1b[1m\x1b[33m";
48const STYLE_INLINE_CODE: &str = "\x1b[48;2;60;60;60m\x1b[38;2;255;255;255m";
49const STYLE_BLOCKQUOTE: &str = "\x1b[38;5;240m";
50const STYLE_LIST_BULLET: &str = "\x1b[33m";
51const STYLE_MATH: &str = "\x1b[36;3m";
52const STYLE_RESET: &str = "\x1b[0m";
53const STYLE_RESET_BG: &str = "\x1b[49m";
54const STYLE_RESET_FG: &str = "\x1b[39m";
55
56const COLOR_CODE_BG: Color = Color::Rgb {
57 r: 30,
58 g: 30,
59 b: 30,
60};
61
62static RE_INVISIBLE: LazyLock<Regex> =
64 LazyLock::new(|| Regex::new(&format!("({}|{})", OSC8_PATTERN, ANSI_REGEX_PATTERN)).unwrap());
65
66const OSC8_PATTERN: &str = r"\x1b]8;;.*?\x1b\\";
69
70static RE_LINK: LazyLock<Regex> = LazyLock::new(|| {
72 Regex::new(
73 r"\[((?:[^\[\]]|\[(?:[^\[\]]|\[[^\[\]]*\])*\])*)\]\(((?:[^()\s]|\((?:[^()\s]|\([^()\s]*\))*\))*)\)",
74 )
75 .unwrap()
76});
77static RE_OSC8: LazyLock<Regex> = LazyLock::new(|| Regex::new(OSC8_PATTERN).unwrap());
78
79static RE_TOKENIZER: LazyLock<Regex> = LazyLock::new(|| {
80 Regex::new(&format!(
81 r"({}|{}|`+|\\[\s\S]|\$[^\$\s](?:[^\$\n]*?[^\$\s])?\$|~~|~|\*\*\*|___|\*\*|__|\*|_|\$|[^~*_`$\\\x1b]+)",
82 OSC8_PATTERN, ANSI_REGEX_PATTERN
83 ))
84 .unwrap()
85});
86
87static RE_SPLIT_ANSI: LazyLock<Regex> = LazyLock::new(|| {
88 let pattern = format!(
89 "({}|{}|\\s+|[^\\s\\x1b]+)",
90 OSC8_PATTERN, ANSI_REGEX_PATTERN
91 );
92 Regex::new(&pattern).unwrap()
93});
94static RE_ANSI_PARTS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\x1b\[([0-9;]*)m").unwrap());
95
96struct ListLevel {
99 source_indent: usize,
100 marker_width: usize,
101}
102
103impl ListLevel {
104 fn new(source_indent: usize, marker_width: usize) -> Self {
105 Self {
106 source_indent,
107 marker_width,
108 }
109 }
110}
111
112struct ListContext {
113 levels: Vec<ListLevel>,
114}
115
116impl ListContext {
117 fn new() -> Self {
118 Self { levels: Vec::new() }
119 }
120
121 fn is_empty(&self) -> bool {
122 self.levels.is_empty()
123 }
124
125 fn structural_width(&self) -> usize {
126 self.levels.iter().map(|l| l.marker_width).sum()
127 }
128
129 fn parent_width(&self) -> usize {
130 if self.levels.is_empty() {
131 0
132 } else {
133 self.levels[..self.levels.len() - 1]
134 .iter()
135 .map(|l| l.marker_width)
136 .sum()
137 }
138 }
139
140 fn last_indent(&self) -> Option<usize> {
141 self.levels.last().map(|l| l.source_indent)
142 }
143
144 fn push(&mut self, source_indent: usize, marker_width: usize) {
145 self.levels
146 .push(ListLevel::new(source_indent, marker_width));
147 }
148
149 fn pop_to_indent(&mut self, indent: usize) {
150 while self.levels.last().is_some_and(|l| l.source_indent > indent) {
151 self.levels.pop();
152 }
153 }
154
155 fn update_last_marker_width(&mut self, marker_width: usize) {
156 if let Some(last) = self.levels.last_mut() {
157 last.marker_width = marker_width;
158 }
159 }
160
161 fn clear(&mut self) {
162 self.levels.clear();
163 }
164}
165
166struct InlineCodeState {
167 ticks: Option<usize>,
168 buffer: String,
169}
170
171impl InlineCodeState {
172 fn new() -> Self {
173 Self {
174 ticks: None,
175 buffer: String::new(),
176 }
177 }
178
179 fn is_active(&self) -> bool {
180 self.ticks.is_some()
181 }
182
183 fn open(&mut self, tick_count: usize) {
184 self.ticks = Some(tick_count);
185 self.buffer.clear();
186 }
187
188 fn feed(&mut self, token: &str) -> Option<String> {
189 if let Some(n) = self.ticks {
190 if token.starts_with('`') && token.len() == n {
191 let result = self.normalize_content();
192 self.ticks = None;
193 self.buffer.clear();
194 return Some(result);
195 }
196 self.buffer.push_str(token);
197 }
198 None
199 }
200
201 fn append_space(&mut self) {
202 if self.is_active() {
203 self.buffer.push(' ');
204 }
205 }
206
207 fn normalize_content(&self) -> String {
208 if self.buffer.len() >= 2
209 && self.buffer.starts_with(' ')
210 && self.buffer.ends_with(' ')
211 && !self.buffer.trim().is_empty()
212 {
213 self.buffer[1..self.buffer.len() - 1].to_string()
214 } else {
215 self.buffer.clone()
216 }
217 }
218
219 fn flush_incomplete(&self) -> Option<(usize, String)> {
220 self.ticks.map(|n| (n, self.buffer.clone()))
221 }
222
223 fn reset(&mut self) {
224 self.ticks = None;
225 self.buffer.clear();
226 }
227}
228
229enum InlineToken {
230 Text(String),
231 Delimiter {
232 char: char,
233 len: usize,
234 can_open: bool,
235 can_close: bool,
236 },
237}
238
239struct InlinePart {
240 token: InlineToken,
241 pre_style: Vec<String>,
242 post_style: Vec<String>,
243}
244
245impl InlinePart {
246 fn text(content: String) -> Self {
247 Self {
248 token: InlineToken::Text(content),
249 pre_style: vec![],
250 post_style: vec![],
251 }
252 }
253
254 fn delimiter(char: char, len: usize, can_open: bool, can_close: bool) -> Self {
255 Self {
256 token: InlineToken::Delimiter {
257 char,
258 len,
259 can_open,
260 can_close,
261 },
262 pre_style: vec![],
263 post_style: vec![],
264 }
265 }
266
267 fn content(&self) -> String {
268 match &self.token {
269 InlineToken::Text(s) => s.clone(),
270 InlineToken::Delimiter { char, len, .. } => char.to_string().repeat(*len),
271 }
272 }
273
274 fn is_delim(&self) -> bool {
275 matches!(self.token, InlineToken::Delimiter { .. })
276 }
277
278 fn delim_char(&self) -> char {
279 match &self.token {
280 InlineToken::Delimiter { char, .. } => *char,
281 _ => '\0',
282 }
283 }
284
285 fn delim_len(&self) -> usize {
286 match &self.token {
287 InlineToken::Delimiter { len, .. } => *len,
288 _ => 0,
289 }
290 }
291
292 fn can_open(&self) -> bool {
293 match &self.token {
294 InlineToken::Delimiter { can_open, .. } => *can_open,
295 _ => false,
296 }
297 }
298
299 fn can_close(&self) -> bool {
300 match &self.token {
301 InlineToken::Delimiter { can_close, .. } => *can_close,
302 _ => false,
303 }
304 }
305
306 fn consume(&mut self, amount: usize) {
307 if let InlineToken::Delimiter { len, .. } = &mut self.token {
308 *len = len.saturating_sub(amount);
309 }
310 }
311}
312
313pub struct MarkdownStreamer {
314 active_fence: Option<(char, usize, usize)>, code_lang: String,
317
318 inline_code: InlineCodeState,
320
321 in_math_block: bool,
323 math_buffer: String,
324
325 in_table: bool,
327 table_header_printed: bool,
328
329 highlighter: Option<HighlightLines<'static>>,
331 line_buffer: String,
332
333 margin: usize,
335 blockquote_depth: usize,
336 list_context: ListContext,
337 pending_newline: bool,
338
339 manual_width: Option<usize>,
341
342 scratch_buffer: String,
344}
345
346impl Default for MarkdownStreamer {
347 fn default() -> Self {
348 Self::new()
349 }
350}
351
352impl MarkdownStreamer {
353 pub fn new() -> Self {
354 Self {
355 active_fence: None,
356 code_lang: "bash".to_string(),
357 inline_code: InlineCodeState::new(),
358 in_math_block: false,
359 math_buffer: String::new(),
360 in_table: false,
361 table_header_printed: false,
362 highlighter: None,
363 line_buffer: String::new(),
364 margin: 2,
365 blockquote_depth: 0,
366 list_context: ListContext::new(),
367 pending_newline: false,
368 manual_width: None,
369 scratch_buffer: String::with_capacity(1024),
370 }
371 }
372
373 pub fn set_width(&mut self, width: usize) {
375 self.manual_width = Some(width);
376 }
377
378 pub fn set_margin(&mut self, margin: usize) {
380 self.margin = margin;
381 }
382
383 fn get_width(&self) -> usize {
384 self.manual_width
385 .unwrap_or_else(crate::console::get_terminal_width)
386 }
387
388 fn visible_width(&self, text: &str) -> usize {
389 UnicodeWidthStr::width(RE_INVISIBLE.replace_all(text, "").as_ref())
390 }
391
392 pub fn print_chunk<W: Write>(&mut self, writer: &mut W, text: &str) -> io::Result<()> {
394 self.line_buffer.push_str(text);
395 while let Some(pos) = self.line_buffer.find('\n') {
396 let line = self.line_buffer[..pos + 1].to_string();
397 self.line_buffer.drain(..pos + 1);
398 self.process_line(writer, &line)?;
399 }
400 Ok(())
401 }
402
403 pub fn flush<W: Write>(&mut self, writer: &mut W) -> io::Result<()> {
405 if !self.line_buffer.is_empty() {
406 let line = std::mem::take(&mut self.line_buffer);
407 self.process_line(writer, &line)?;
408 }
409
410 self.flush_pending_inline(writer)?;
411 self.commit_newline(writer)?;
412 writer.flush()
413 }
414
415 fn commit_newline<W: Write>(&mut self, writer: &mut W) -> io::Result<()> {
416 if self.pending_newline {
417 queue!(writer, Print("\n"))?;
418 self.pending_newline = false;
419 }
420 Ok(())
421 }
422
423 fn flush_pending_inline<W: Write>(&mut self, writer: &mut W) -> io::Result<()> {
424 if let Some((ticks, buffer)) = self.inline_code.flush_incomplete() {
425 queue!(writer, Print("`".repeat(ticks)))?;
426 queue!(writer, Print(&buffer))?;
427 self.inline_code.reset();
428 }
429 Ok(())
430 }
431
432 fn process_line<W: Write>(&mut self, w: &mut W, raw_line: &str) -> io::Result<()> {
434 let expanded = self.expand_tabs(raw_line);
435 let trimmed = expanded.trim_end();
436
437 if self.try_handle_fence(w, &expanded, trimmed)? {
439 return Ok(());
440 }
441 if self.try_handle_math(w, trimmed)? {
442 return Ok(());
443 }
444 if self.try_handle_table(w, trimmed)? {
445 return Ok(());
446 }
447
448 let mut content = expanded.as_str();
450 self.blockquote_depth = 0;
451 while let Some(caps) = RE_BLOCKQUOTE.captures(content) {
452 self.blockquote_depth += 1;
453 content = caps.get(2).map_or("", |m| m.as_str());
454 }
455
456 let prefix = self.build_block_prefix();
457
458 let term_width = self.get_width();
459 let prefix_width = self.margin + (self.blockquote_depth * 2);
460 let avail_width = term_width.saturating_sub(prefix_width + self.margin);
461
462 let clean = content.trim_end();
466 if self.try_handle_header(w, clean, &prefix, avail_width)? {
467 return Ok(());
468 }
469 if self.try_handle_hr(w, clean, &prefix, avail_width)? {
470 return Ok(());
471 }
472 if self.try_handle_list(w, clean, &prefix, avail_width)? {
473 return Ok(());
474 }
475
476 self.render_standard_text(w, content, &prefix, avail_width)
478 }
479
480 fn try_handle_fence<W: Write>(
483 &mut self,
484 w: &mut W,
485 full: &str,
486 trimmed: &str,
487 ) -> io::Result<bool> {
488 let match_data = RE_CODE_FENCE.captures(trimmed);
489
490 if let Some((f_char, min_len, _)) = self.active_fence {
492 if let Some(caps) = &match_data {
493 let fence = &caps[2];
494 if fence.starts_with(f_char) && fence.len() >= min_len && caps[3].trim().is_empty()
495 {
496 self.active_fence = None;
497 self.commit_newline(w)?;
498 queue!(w, ResetColor)?;
499 self.pending_newline = true;
500 return Ok(true);
501 }
502 }
503 self.render_code_line(w, full)?;
504 return Ok(true);
505 }
506
507 if let Some(caps) = match_data {
509 let fence = &caps[2];
510 let indent_len = caps[1].len();
511 let info = caps[3].trim();
512 if let Some(f_char) = fence.chars().next()
513 && (f_char != '`' || !info.contains('`'))
514 {
515 self.flush_pending_inline(w)?;
516 self.commit_newline(w)?;
517 self.list_context.pop_to_indent(indent_len);
518 self.active_fence = Some((f_char, fence.len(), indent_len));
519 let lang = info.split_whitespace().next().unwrap_or("bash");
520 self.code_lang = lang.to_string();
521 self.start_highlighter(&self.code_lang.clone());
522 return Ok(true);
523 }
524 }
525 Ok(false)
526 }
527
528 fn try_handle_math<W: Write>(&mut self, w: &mut W, trimmed: &str) -> io::Result<bool> {
529 if RE_MATH_BLOCK.is_match(trimmed) {
530 if self.in_math_block {
531 self.in_math_block = false;
532 let converted = unicodeit::replace(&self.math_buffer);
533 let p_width = self.margin + (self.blockquote_depth * 2);
534 let avail = self.get_width().saturating_sub(p_width + self.margin);
535 let padding = avail.saturating_sub(self.visible_width(&converted)) / 2;
536
537 self.commit_newline(w)?;
538 queue!(
539 w,
540 Print(" ".repeat(self.margin + padding)),
541 Print(STYLE_MATH),
542 Print(converted),
543 Print(STYLE_RESET)
544 )?;
545 self.pending_newline = true;
546 self.math_buffer.clear();
547 } else {
548 self.flush_pending_inline(w)?;
549 self.commit_newline(w)?;
550 self.exit_block_context();
551 self.in_math_block = true;
552 }
553 return Ok(true);
554 }
555 if self.in_math_block {
556 self.math_buffer.push_str(trimmed);
557 self.math_buffer.push(' ');
558 return Ok(true);
559 }
560 Ok(false)
561 }
562
563 fn try_handle_table<W: Write>(&mut self, w: &mut W, trimmed: &str) -> io::Result<bool> {
564 if self.in_table && RE_TABLE_SEP.is_match(trimmed) {
565 self.table_header_printed = true;
566 return Ok(true);
567 }
568 if RE_TABLE_ROW.is_match(trimmed) {
569 if !self.in_table {
570 self.flush_pending_inline(w)?;
571 self.commit_newline(w)?;
572 self.exit_block_context();
573 self.in_table = true;
574 }
575 self.render_stream_table_row(w, trimmed)?;
576 return Ok(true);
577 }
578 self.in_table = false;
579 self.table_header_printed = false;
580 Ok(false)
581 }
582
583 fn try_handle_header<W: Write>(
584 &mut self,
585 w: &mut W,
586 clean: &str,
587 prefix: &str,
588 avail: usize,
589 ) -> io::Result<bool> {
590 if let Some(caps) = RE_HEADER.captures(clean) {
591 self.flush_pending_inline(w)?;
592 self.commit_newline(w)?;
593 let level = caps.get(1).map_or(0, |m| m.len());
594 let text = self.clean_atx_header_text(caps.get(2).map_or("", |m| m.as_str()));
595 self.exit_block_context();
596
597 queue!(w, Print(prefix))?;
598 if level <= 2 {
599 queue!(w, Print("\n"))?;
600 }
601
602 self.scratch_buffer.clear();
603 let style = match level {
604 1 => STYLE_H1,
605 2 => STYLE_H2,
606 3 => STYLE_H3,
607 _ => STYLE_H_DEFAULT,
608 };
609 self.render_inline(text, None, Some(style));
610
611 if level <= 2 {
612 let lines = self.wrap_ansi(&self.scratch_buffer, avail);
613 for (i, line) in lines.iter().enumerate() {
614 let pad = avail.saturating_sub(self.visible_width(line)) / 2;
615 if i > 0 {
616 queue!(w, Print("\n"), Print(prefix))?;
617 }
618 queue!(
619 w,
620 Print(" ".repeat(pad)),
621 Print(format!("{}{}{}", style, line, STYLE_RESET)),
622 ResetColor
623 )?;
624 }
625 if level == 1 {
626 queue!(w, Print("\n"), Print(prefix), Print("─".repeat(avail)))?;
627 }
628 self.pending_newline = true;
629 } else {
630 queue!(
631 w,
632 Print(style),
633 Print(&self.scratch_buffer),
634 Print(STYLE_RESET)
635 )?;
636 self.pending_newline = true;
637 }
638 return Ok(true);
639 }
640 Ok(false)
641 }
642
643 fn try_handle_list<W: Write>(
644 &mut self,
645 w: &mut W,
646 clean: &str,
647 prefix: &str,
648 avail: usize,
649 ) -> io::Result<bool> {
650 if let Some(caps) = RE_LIST.captures(clean) {
651 self.flush_pending_inline(w)?;
652 self.commit_newline(w)?;
653 let indent = caps.get(1).map_or(0, |m| m.len());
654 let bullet = caps.get(2).map_or("-", |m| m.as_str());
655 let separator = caps.get(3).map_or(" ", |m| m.as_str());
656 let text = caps.get(4).map_or("", |m| m.as_str());
657
658 let is_ord = bullet.chars().any(|c| c.is_numeric());
659 let disp_bullet = if is_ord { bullet } else { "•" };
660 let marker_width = self.visible_width(disp_bullet) + separator.len();
661
662 let last_indent = self.list_context.last_indent().unwrap_or(0);
663 if self.list_context.is_empty() || indent > last_indent {
664 self.list_context.push(indent, marker_width);
665 } else if indent < last_indent {
666 self.list_context.pop_to_indent(indent);
667 if self.list_context.last_indent().is_some_and(|d| d != indent) {
668 self.list_context.push(indent, marker_width);
669 }
670 } else {
671 self.list_context.update_last_marker_width(marker_width);
673 }
674
675 let full_stack_width = self.list_context.structural_width();
676 let parent_width = self.list_context.parent_width();
677
678 let hang_indent = " ".repeat(full_stack_width);
679 let content_width = avail.saturating_sub(full_stack_width);
680
681 queue!(
682 w,
683 Print(prefix),
684 Print(" ".repeat(parent_width)),
685 Print(STYLE_LIST_BULLET),
686 Print(disp_bullet),
687 Print(STYLE_RESET),
688 Print(separator)
689 )?;
690
691 if let Some(fcaps) = RE_CODE_FENCE.captures(text) {
693 queue!(w, Print("\n"))?;
694
695 let fence_chars = &fcaps[2];
696 let info = fcaps[3].trim();
697
698 if let Some(f_char) = fence_chars.chars().next() {
699 self.active_fence = Some((f_char, fence_chars.len(), 0));
700
701 let lang = info.split_whitespace().next().unwrap_or("bash");
702 self.code_lang = lang.to_string();
703 self.start_highlighter(&self.code_lang.clone());
704 }
705 return Ok(true);
706 }
707
708 self.scratch_buffer.clear();
709 self.render_inline(text, None, None);
710 let lines = self.wrap_ansi(&self.scratch_buffer, content_width);
711
712 if lines.is_empty() {
713 self.pending_newline = true;
714 } else {
715 for (i, line) in lines.iter().enumerate() {
716 if i > 0 {
717 queue!(w, Print("\n"), Print(prefix), Print(&hang_indent))?;
718 }
719 queue!(w, Print(line), ResetColor)?;
720 }
721 self.pending_newline = true;
722 }
723 return Ok(true);
724 }
725 Ok(false)
726 }
727
728 fn try_handle_hr<W: Write>(
729 &mut self,
730 w: &mut W,
731 clean: &str,
732 prefix: &str,
733 avail: usize,
734 ) -> io::Result<bool> {
735 if RE_HR.is_match(clean) {
736 self.flush_pending_inline(w)?;
737 self.commit_newline(w)?;
738 queue!(
739 w,
740 Print(prefix),
741 SetForegroundColor(Color::DarkGrey),
742 Print("─".repeat(avail)),
743 ResetColor
744 )?;
745 self.pending_newline = true;
746 self.exit_block_context();
747 return Ok(true);
748 }
749 Ok(false)
750 }
751
752 fn render_standard_text<W: Write>(
753 &mut self,
754 w: &mut W,
755 content: &str,
756 prefix: &str,
757 avail: usize,
758 ) -> io::Result<()> {
759 self.commit_newline(w)?;
760 let mut line_content = content.trim_end_matches(['\n', '\r']);
761 if line_content.trim().is_empty() && content.ends_with('\n') {
762 self.exit_block_context();
763 if self.blockquote_depth > 0 {
764 queue!(w, Print(prefix))?;
765 }
766 self.pending_newline = true;
767 return Ok(());
768 }
769
770 if !line_content.is_empty() || self.inline_code.is_active() {
771 let mut eff_prefix = self.build_block_prefix();
772 if !self.list_context.is_empty() {
773 let current_indent = line_content.chars().take_while(|c| *c == ' ').count();
774 if current_indent == 0 {
775 self.list_context.clear();
776 } else {
777 self.list_context.pop_to_indent(current_indent);
778 }
779
780 if !self.list_context.is_empty() {
781 let structural_indent = self.list_context.structural_width();
782 eff_prefix.push_str(&" ".repeat(structural_indent));
783
784 let skip = current_indent.min(structural_indent);
787 line_content = &line_content[skip..];
788 }
789 }
790
791 self.scratch_buffer.clear();
792 self.render_inline(line_content, None, None);
793 self.inline_code.append_space();
794
795 let lines = self.wrap_ansi(&self.scratch_buffer, avail);
796 for (i, line) in lines.iter().enumerate() {
797 if i > 0 {
798 queue!(w, Print("\n"))?;
799 }
800 queue!(
801 w,
802 ResetColor,
803 SetAttribute(Attribute::Reset),
804 Print(&eff_prefix),
805 Print(line),
806 ResetColor
807 )?;
808 }
809 if !lines.is_empty() {
810 self.pending_newline = true;
811 }
812 }
813 Ok(())
814 }
815
816 fn exit_block_context(&mut self) {
817 self.list_context.clear();
818 self.in_table = false;
819 self.table_header_printed = false;
820 }
821
822 fn wrap_ansi(&self, text: &str, width: usize) -> Vec<String> {
823 let mut lines = Vec::new();
824 let mut current_line = String::new();
825 let mut current_len = 0;
826 let mut active_codes: Vec<String> = Vec::new();
827
828 for caps in RE_SPLIT_ANSI.captures_iter(text) {
829 let token = caps.get(1).unwrap().as_str();
830 if token.starts_with("\x1b") {
831 current_line.push_str(token);
832 self.update_ansi_state(&mut active_codes, token);
833 } else {
834 let mut token_str = token;
835 let mut token_len = UnicodeWidthStr::width(token_str);
836
837 while current_len + token_len > width && width > 0 {
838 if current_len == 0 {
839 let mut split_idx = 0;
841 let mut split_len = 0;
842 for (idx, c) in token_str.char_indices() {
843 let c_w = c.width().unwrap_or(0);
844 if split_len + c_w > width {
845 break;
846 }
847 split_idx = idx + c.len_utf8();
848 split_len += c_w;
849 }
850 if split_idx == 0 {
851 split_idx = token_str.chars().next().map_or(0, |c| c.len_utf8());
852 }
853 if split_idx == 0 {
854 break;
855 }
856
857 current_line.push_str(&token_str[..split_idx]);
858 lines.push(current_line);
859 current_line = active_codes.join("");
860 token_str = &token_str[split_idx..];
861 token_len = UnicodeWidthStr::width(token_str);
862 current_len = 0;
863 } else if !token_str.trim().is_empty() {
864 lines.push(current_line);
865 current_line = active_codes.join("");
866 current_len = 0;
867 } else {
868 token_str = "";
869 token_len = 0;
870 }
871 }
872 if !token_str.is_empty() {
873 current_line.push_str(token_str);
874 current_len += token_len;
875 }
876 }
877 }
878 if !current_line.is_empty() {
879 lines.push(current_line);
880 }
881 lines
882 }
883
884 fn update_ansi_state(&self, state: &mut Vec<String>, code: &str) {
885 if RE_OSC8.is_match(code) {
886 return;
887 }
888 if let Some(caps) = RE_ANSI_PARTS.captures(code) {
889 let content = caps.get(1).map_or("", |m| m.as_str());
890 if content == "0" || content.is_empty() {
891 state.clear();
892 return;
893 }
894
895 let num: i32 = content
896 .split(';')
897 .next()
898 .unwrap_or("0")
899 .parse()
900 .unwrap_or(0);
901 let category = match num {
902 1 | 22 => "bold",
903 3 | 23 => "italic",
904 4 | 24 => "underline",
905 30..=39 | 90..=97 => "fg",
906 40..=49 | 100..=107 => "bg",
907 _ => "other",
908 };
909 if category != "other" {
910 state.retain(|exist| {
911 let e_num: i32 = RE_ANSI_PARTS
912 .captures(exist)
913 .and_then(|c| c.get(1))
914 .map_or("0", |m| m.as_str())
915 .split(';')
916 .next()
917 .unwrap_or("0")
918 .parse()
919 .unwrap_or(0);
920 let e_cat = match e_num {
921 1 | 22 => "bold",
922 3 | 23 => "italic",
923 4 | 24 => "underline",
924 30..=39 | 90..=97 => "fg",
925 40..=49 | 100..=107 => "bg",
926 _ => "other",
927 };
928 e_cat != category
929 });
930 }
931 state.push(code.to_string());
932 }
933 }
934
935 fn render_code_line<W: Write>(&mut self, w: &mut W, line: &str) -> io::Result<()> {
936 self.commit_newline(w)?;
937 let raw_line = line.trim_end_matches(&['\r', '\n'][..]);
938
939 let fence_indent = self.active_fence.map(|(_, _, i)| i).unwrap_or(0);
940
941 let skip = raw_line
943 .chars()
944 .take(fence_indent)
945 .take_while(|&c| c == ' ')
946 .count();
947 let line_content = &raw_line[skip..];
948
949 let mut prefix = " ".repeat(self.margin);
950 if !self.list_context.is_empty() {
951 let indent_width = self.list_context.structural_width();
952 prefix.push_str(&" ".repeat(indent_width));
953 }
954
955 let avail_width = self.get_width().saturating_sub(prefix.len() + self.margin);
956
957 let mut spans = Vec::new();
958 if let Some(h) = &mut self.highlighter {
959 if let Ok(ranges) = h.highlight_line(line_content, &SYNTAX_SET) {
960 spans = ranges;
961 } else {
962 spans.push((syntect::highlighting::Style::default(), line_content));
963 }
964 } else {
965 spans.push((syntect::highlighting::Style::default(), line_content));
966 }
967
968 self.scratch_buffer.clear();
970 for (style, text) in spans {
971 let _ = write!(
972 self.scratch_buffer,
973 "\x1b[38;2;{};{};{}m{}",
974 style.foreground.r, style.foreground.g, style.foreground.b, text
975 );
976 }
977
978 let content_width = self.visible_width(line_content);
980
981 if content_width <= avail_width {
982 let pad = avail_width.saturating_sub(content_width);
984 queue!(
985 w,
986 Print(&prefix),
987 SetBackgroundColor(COLOR_CODE_BG),
988 Print(&self.scratch_buffer),
989 Print(" ".repeat(pad)),
990 ResetColor
991 )?;
992 } else {
993 let wrapped_lines = self.wrap_ansi(&self.scratch_buffer, avail_width);
995
996 if wrapped_lines.is_empty() {
997 queue!(
998 w,
999 Print(&prefix),
1000 SetBackgroundColor(COLOR_CODE_BG),
1001 Print(" ".repeat(avail_width)),
1002 ResetColor
1003 )?;
1004 } else {
1005 for (i, line) in wrapped_lines.iter().enumerate() {
1006 if i > 0 {
1007 queue!(w, Print("\n"))?;
1008 }
1009 let vis_len = self.visible_width(line);
1010 let pad = avail_width.saturating_sub(vis_len);
1011
1012 queue!(
1013 w,
1014 Print(&prefix),
1015 SetBackgroundColor(COLOR_CODE_BG),
1016 Print(line),
1017 Print(" ".repeat(pad)),
1018 ResetColor
1019 )?;
1020 }
1021 }
1022 }
1023 self.pending_newline = true;
1024 Ok(())
1025 }
1026
1027 fn render_stream_table_row<W: Write>(&mut self, w: &mut W, row_str: &str) -> io::Result<()> {
1028 self.commit_newline(w)?;
1029 let term_width = self.get_width();
1030 let cells: Vec<&str> = row_str.trim().trim_matches('|').split('|').collect();
1031 if cells.is_empty() {
1032 return Ok(());
1033 }
1034
1035 let prefix_width = self.margin + (self.blockquote_depth * 2);
1036 let cell_overhead = (cells.len() * 3).saturating_sub(1);
1037 let avail = term_width.saturating_sub(prefix_width + self.margin + cell_overhead);
1038 if avail == 0 {
1039 return Ok(());
1040 }
1041 let base_w = avail / cells.len();
1042 let rem = avail % cells.len();
1043
1044 let bg = if !self.table_header_printed {
1045 Color::Rgb {
1046 r: 60,
1047 g: 60,
1048 b: 80,
1049 }
1050 } else {
1051 COLOR_CODE_BG
1052 };
1053 let mut wrapped_cells = Vec::new();
1054 let mut max_h = 1;
1055
1056 for (i, cell) in cells.iter().enumerate() {
1057 let width = std::cmp::max(
1058 1,
1059 if i == cells.len() - 1 {
1060 base_w + rem
1061 } else {
1062 base_w
1063 },
1064 );
1065 self.scratch_buffer.clear();
1066 if !self.table_header_printed {
1067 self.scratch_buffer.push_str("\x1b[1;33m");
1068 }
1069 self.render_inline(
1070 cell.trim(),
1071 Some(bg),
1072 if !self.table_header_printed {
1073 Some("\x1b[1;33m")
1074 } else {
1075 None
1076 },
1077 );
1078 if !self.table_header_printed {
1079 self.scratch_buffer.push_str("\x1b[0m");
1080 }
1081
1082 let lines = self.wrap_ansi(&self.scratch_buffer, width);
1083 if lines.len() > max_h {
1084 max_h = lines.len();
1085 }
1086 wrapped_cells.push((lines, width));
1087 }
1088
1089 let prefix = self.build_block_prefix();
1090
1091 for i in 0..max_h {
1092 if i > 0 {
1093 queue!(w, Print("\n"))?;
1094 }
1095 queue!(w, Print(&prefix))?;
1096 for (col, (lines, width)) in wrapped_cells.iter().enumerate() {
1097 let text = lines.get(i).map(|s| s.as_str()).unwrap_or("");
1098 let pad = width.saturating_sub(self.visible_width(text));
1099 queue!(
1100 w,
1101 SetBackgroundColor(bg),
1102 Print(" "),
1103 Print(text),
1104 SetBackgroundColor(bg),
1105 Print(" ".repeat(pad + 1)),
1106 ResetColor
1107 )?;
1108 if col < cells.len() - 1 {
1109 queue!(
1110 w,
1111 SetBackgroundColor(bg),
1112 SetForegroundColor(Color::White),
1113 Print("│"),
1114 ResetColor
1115 )?;
1116 }
1117 }
1118 }
1119 self.pending_newline = true;
1120 self.table_header_printed = true;
1121 Ok(())
1122 }
1123
1124 pub fn render_inline(&mut self, text: &str, def_bg: Option<Color>, restore_fg: Option<&str>) {
1125 static RE_AUTOLINK: LazyLock<Regex> = LazyLock::new(|| {
1127 Regex::new(r"<([a-zA-Z][a-zA-Z0-9+.-]{1,31}:[^<> \x00-\x1f]+)>").unwrap()
1128 });
1129
1130 let text_autolinked = RE_AUTOLINK.replace_all(text, |c: ®ex::Captures| {
1131 let url = &c[1];
1132 format!("\x1b]8;;{}\x1b\\{}\x1b]8;;\x1b\\", url, url)
1133 });
1134
1135 let text_linked = RE_LINK.replace_all(&text_autolinked, |c: ®ex::Captures| {
1137 format!(
1138 "\x1b]8;;{}\x1b\\\x1b[33;4m{}\x1b[24;39m\x1b]8;;\x1b\\",
1139 &c[2], &c[1]
1140 )
1141 });
1142
1143 let mut parts: Vec<InlinePart> = Vec::new();
1144 let caps_iter = RE_TOKENIZER.captures_iter(&text_linked);
1145 let tokens_raw: Vec<&str> = caps_iter.map(|c| c.get(1).unwrap().as_str()).collect();
1146
1147 for (i, tok) in tokens_raw.iter().enumerate() {
1149 if self.inline_code.is_active() {
1150 if tok.starts_with('`') {
1151 if let Some(content) = self.inline_code.feed(tok) {
1152 let formatted =
1153 self.format_inline_code_content(&content, def_bg, restore_fg);
1154 parts.push(InlinePart::text(formatted));
1155 }
1156 } else {
1157 self.inline_code.feed(tok);
1158 }
1159 continue;
1160 }
1161
1162 if tok.starts_with('`') {
1163 self.inline_code.open(tok.len());
1164 continue;
1165 }
1166
1167 if tok.starts_with('\\') && tok.len() > 1 {
1168 parts.push(InlinePart::text(tok[1..].to_string()));
1169 continue;
1170 }
1171
1172 if tok.starts_with('$') && tok.ends_with('$') && tok.len() > 1 {
1173 parts.push(InlinePart::text(unicodeit::replace(&tok[1..tok.len() - 1])));
1174 continue;
1175 }
1176
1177 if let Some(c) = tok.chars().next()
1178 && (c == '*' || c == '_' || c == '~')
1179 {
1180 let prev_char = if i > 0 {
1181 tokens_raw[i - 1].chars().last().unwrap_or(' ')
1182 } else {
1183 ' '
1184 };
1185 let next_char = if i + 1 < tokens_raw.len() {
1186 tokens_raw[i + 1].chars().next().unwrap_or(' ')
1187 } else {
1188 ' '
1189 };
1190
1191 let is_ws_next = next_char.is_whitespace();
1193 let is_ws_prev = prev_char.is_whitespace();
1194 let is_punct_next = !next_char.is_alphanumeric() && !is_ws_next;
1195 let is_punct_prev = !prev_char.is_alphanumeric() && !is_ws_prev;
1196 let left_flanking =
1197 !is_ws_next && (!is_punct_next || (is_ws_prev || is_punct_prev));
1198 let right_flanking =
1199 !is_ws_prev && (!is_punct_prev || (is_ws_next || is_punct_next));
1200
1201 let (can_open, can_close) = if c == '_' {
1202 (
1203 left_flanking && (!right_flanking || is_punct_prev),
1204 right_flanking && (!left_flanking || is_punct_next),
1205 )
1206 } else {
1207 (left_flanking, right_flanking)
1208 };
1209
1210 parts.push(InlinePart::delimiter(c, tok.len(), can_open, can_close));
1211 } else {
1212 parts.push(InlinePart::text(tok.to_string()));
1213 }
1214 }
1215
1216 self.resolve_delimiters(&mut parts);
1218
1219 for part in parts {
1221 for s in &part.pre_style {
1222 self.scratch_buffer.push_str(s);
1223 }
1224 self.scratch_buffer.push_str(&part.content());
1225 for s in &part.post_style {
1226 self.scratch_buffer.push_str(s);
1227 }
1228 }
1229 }
1230
1231 fn resolve_delimiters(&self, parts: &mut [InlinePart]) {
1232 let mut stack: Vec<usize> = Vec::new();
1233
1234 for i in 0..parts.len() {
1235 if !parts[i].is_delim() {
1236 continue;
1237 }
1238
1239 if parts[i].can_close() {
1240 let mut stack_idx = stack.len();
1241 while stack_idx > 0 {
1242 let open_pos = stack_idx - 1;
1243 let open_idx = stack[open_pos];
1244
1245 if parts[open_idx].delim_char() == parts[i].delim_char()
1246 && parts[open_idx].can_open()
1247 {
1248 if (parts[open_idx].can_open() && parts[open_idx].can_close())
1250 || (parts[i].can_open() && parts[i].can_close())
1251 {
1252 let sum = parts[open_idx].delim_len() + parts[i].delim_len();
1253 if sum.is_multiple_of(3)
1254 && (!parts[open_idx].delim_len().is_multiple_of(3)
1255 || !parts[i].delim_len().is_multiple_of(3))
1256 {
1257 stack_idx -= 1;
1258 continue;
1259 }
1260 }
1261
1262 if open_idx + 1 == i {
1264 stack_idx -= 1;
1265 continue;
1266 }
1267
1268 let open_len = parts[open_idx].delim_len();
1270 let close_len = parts[i].delim_len();
1271 let use_len = if close_len == 3 && open_len == 3 {
1272 1
1273 } else if close_len >= 2 && open_len >= 2 {
1274 2
1275 } else {
1276 1
1277 };
1278
1279 let (style_on, style_off) = match (parts[open_idx].delim_char(), use_len) {
1280 ('~', _) => ("\x1b[9m", "\x1b[29m"),
1281 ('_', 1) => ("\x1b[4m", "\x1b[24m"),
1282 (_, 1) => ("\x1b[3m", "\x1b[23m"),
1283 (_, 2) => ("\x1b[1m", "\x1b[22m"),
1284 _ => ("", ""),
1285 };
1286
1287 if use_len == 1 {
1289 parts[open_idx].pre_style.push(style_on.to_string());
1290 parts[i].post_style.push(style_off.to_string());
1291 } else {
1292 parts[open_idx].post_style.push(style_on.to_string());
1293 parts[i].pre_style.push(style_off.to_string());
1294 }
1295
1296 parts[open_idx].consume(use_len);
1298 parts[i].consume(use_len);
1299
1300 if parts[open_idx].delim_len() == 0 {
1302 stack.remove(open_pos);
1303 stack_idx -= 1;
1304 }
1305
1306 if parts[i].delim_len() == 0 {
1307 break;
1308 }
1309 } else {
1310 stack_idx -= 1;
1311 }
1312 }
1313 }
1314
1315 if parts[i].delim_len() > 0 && parts[i].can_open() {
1316 stack.push(i);
1317 }
1318 }
1319 }
1320
1321 fn build_block_prefix(&self) -> String {
1322 let mut prefix = " ".repeat(self.margin);
1323 if self.blockquote_depth > 0 {
1324 prefix.push_str(STYLE_BLOCKQUOTE);
1325 for _ in 0..self.blockquote_depth {
1326 prefix.push_str("│ ");
1327 }
1328 prefix.push_str(STYLE_RESET);
1329 }
1330 prefix
1331 }
1332
1333 fn format_inline_code_content(
1334 &self,
1335 content: &str,
1336 def_bg: Option<Color>,
1337 restore_fg: Option<&str>,
1338 ) -> String {
1339 let mut out = String::new();
1340 let _ = write!(out, "{}{}", STYLE_INLINE_CODE, content);
1341 if let Some(Color::Rgb { r, g, b }) = def_bg {
1342 let _ = write!(out, "\x1b[48;2;{};{};{}m", r, g, b);
1343 } else {
1344 out.push_str(STYLE_RESET_BG);
1345 }
1346 out.push_str(restore_fg.unwrap_or(STYLE_RESET_FG));
1347 out
1348 }
1349
1350 fn expand_tabs(&self, line: &str) -> String {
1351 let mut expanded = String::with_capacity(line.len());
1352 let mut col = 0;
1353 for c in line.chars() {
1354 if c == '\t' {
1355 let n = 4 - (col % 4);
1356 expanded.push_str(&" ".repeat(n));
1357 col += n;
1358 } else {
1359 expanded.push(c);
1360 col += UnicodeWidthChar::width(c).unwrap_or(0);
1361 }
1362 }
1363 expanded
1364 }
1365
1366 fn clean_atx_header_text<'a>(&self, text: &'a str) -> &'a str {
1367 let mut end = text.len();
1369 let bytes = text.as_bytes();
1370 while end > 0 && bytes[end - 1] == b'#' {
1371 end -= 1;
1372 }
1373 if end > 0 && end < text.len() && bytes[end - 1] == b' ' {
1374 &text[..end - 1]
1375 } else if end == 0 {
1376 ""
1377 } else {
1378 &text[..end]
1379 }
1380 }
1381
1382 fn start_highlighter(&mut self, lang: &str) {
1383 let ss = &*SYNTAX_SET;
1384 let syntax = ss
1385 .find_syntax_by_token(lang)
1386 .unwrap_or_else(|| ss.find_syntax_plain_text());
1387 self.highlighter = Some(HighlightLines::new(syntax, &THEME));
1388 }
1389}