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_RESET: &str = "\x1b[0m";
50const STYLE_RESET_BG: &str = "\x1b[49m";
51const STYLE_RESET_FG: &str = "\x1b[39m";
52
53static RE_INVISIBLE: LazyLock<Regex> =
55 LazyLock::new(|| Regex::new(&format!("({}|{})", OSC8_PATTERN, ANSI_REGEX_PATTERN)).unwrap());
56
57const OSC8_PATTERN: &str = r"\x1b]8;;.*?\x1b\\";
60
61static RE_LINK: LazyLock<Regex> = LazyLock::new(|| {
63 Regex::new(
64 r"\[((?:[^\[\]]|\[(?:[^\[\]]|\[[^\[\]]*\])*\])*)\]\(((?:[^()\s]|\((?:[^()\s]|\([^()\s]*\))*\))*)\)",
65 )
66 .unwrap()
67});
68static RE_OSC8: LazyLock<Regex> = LazyLock::new(|| Regex::new(OSC8_PATTERN).unwrap());
69
70static RE_TOKENIZER: LazyLock<Regex> = LazyLock::new(|| {
71 Regex::new(&format!(
72 r"({}|{}|`+|\\[\s\S]|\$[^\$\s](?:[^\$\n]*?[^\$\s])?\$|~~|~|\*\*\*|___|\*\*|__|\*|_|\$|[^~*_`$\\\x1b]+)",
73 OSC8_PATTERN, ANSI_REGEX_PATTERN
74 ))
75 .unwrap()
76});
77
78static RE_SPLIT_ANSI: LazyLock<Regex> = LazyLock::new(|| {
79 let pattern = format!(
80 "({}|{}|\\s+|[^\\s\\x1b]+)",
81 OSC8_PATTERN, ANSI_REGEX_PATTERN
82 );
83 Regex::new(&pattern).unwrap()
84});
85static RE_ANSI_PARTS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\x1b\[([0-9;]*)m").unwrap());
86
87struct ListLevel {
90 source_indent: usize,
91 marker_width: usize,
92}
93
94impl ListLevel {
95 fn new(source_indent: usize, marker_width: usize) -> Self {
96 Self {
97 source_indent,
98 marker_width,
99 }
100 }
101}
102
103struct ListContext {
104 levels: Vec<ListLevel>,
105}
106
107impl ListContext {
108 fn new() -> Self {
109 Self { levels: Vec::new() }
110 }
111
112 fn is_empty(&self) -> bool {
113 self.levels.is_empty()
114 }
115
116 fn structural_width(&self) -> usize {
117 self.levels.iter().map(|l| l.marker_width).sum()
118 }
119
120 fn parent_width(&self) -> usize {
121 if self.levels.is_empty() {
122 0
123 } else {
124 self.levels[..self.levels.len() - 1]
125 .iter()
126 .map(|l| l.marker_width)
127 .sum()
128 }
129 }
130
131 fn last_indent(&self) -> Option<usize> {
132 self.levels.last().map(|l| l.source_indent)
133 }
134
135 fn push(&mut self, source_indent: usize, marker_width: usize) {
136 self.levels
137 .push(ListLevel::new(source_indent, marker_width));
138 }
139
140 fn pop_to_indent(&mut self, indent: usize) {
141 while self.levels.last().is_some_and(|l| l.source_indent > indent) {
142 self.levels.pop();
143 }
144 }
145
146 fn update_last_marker_width(&mut self, marker_width: usize) {
147 if let Some(last) = self.levels.last_mut() {
148 last.marker_width = marker_width;
149 }
150 }
151
152 fn clear(&mut self) {
153 self.levels.clear();
154 }
155}
156
157struct InlineCodeState {
158 ticks: Option<usize>,
159 buffer: String,
160}
161
162impl InlineCodeState {
163 fn new() -> Self {
164 Self {
165 ticks: None,
166 buffer: String::new(),
167 }
168 }
169
170 fn is_active(&self) -> bool {
171 self.ticks.is_some()
172 }
173
174 fn open(&mut self, tick_count: usize) {
175 self.ticks = Some(tick_count);
176 self.buffer.clear();
177 }
178
179 fn feed(&mut self, token: &str) -> Option<String> {
180 if let Some(n) = self.ticks {
181 if token.starts_with('`') && token.len() == n {
182 let result = self.normalize_content();
183 self.ticks = None;
184 self.buffer.clear();
185 return Some(result);
186 }
187 self.buffer.push_str(token);
188 }
189 None
190 }
191
192 fn append_space(&mut self) {
193 if self.is_active() {
194 self.buffer.push(' ');
195 }
196 }
197
198 fn normalize_content(&self) -> String {
199 if self.buffer.len() >= 2
200 && self.buffer.starts_with(' ')
201 && self.buffer.ends_with(' ')
202 && !self.buffer.trim().is_empty()
203 {
204 self.buffer[1..self.buffer.len() - 1].to_string()
205 } else {
206 self.buffer.clone()
207 }
208 }
209
210 fn flush_incomplete(&self) -> Option<(usize, String)> {
211 self.ticks.map(|n| (n, self.buffer.clone()))
212 }
213
214 fn reset(&mut self) {
215 self.ticks = None;
216 self.buffer.clear();
217 }
218}
219
220enum InlineToken {
221 Text(String),
222 Delimiter {
223 char: char,
224 len: usize,
225 can_open: bool,
226 can_close: bool,
227 },
228}
229
230struct InlinePart {
231 token: InlineToken,
232 pre_style: Vec<String>,
233 post_style: Vec<String>,
234}
235
236impl InlinePart {
237 fn text(content: String) -> Self {
238 Self {
239 token: InlineToken::Text(content),
240 pre_style: vec![],
241 post_style: vec![],
242 }
243 }
244
245 fn delimiter(char: char, len: usize, can_open: bool, can_close: bool) -> Self {
246 Self {
247 token: InlineToken::Delimiter {
248 char,
249 len,
250 can_open,
251 can_close,
252 },
253 pre_style: vec![],
254 post_style: vec![],
255 }
256 }
257
258 fn content(&self) -> String {
259 match &self.token {
260 InlineToken::Text(s) => s.clone(),
261 InlineToken::Delimiter { char, len, .. } => char.to_string().repeat(*len),
262 }
263 }
264
265 fn is_delim(&self) -> bool {
266 matches!(self.token, InlineToken::Delimiter { .. })
267 }
268
269 fn delim_char(&self) -> char {
270 match &self.token {
271 InlineToken::Delimiter { char, .. } => *char,
272 _ => '\0',
273 }
274 }
275
276 fn delim_len(&self) -> usize {
277 match &self.token {
278 InlineToken::Delimiter { len, .. } => *len,
279 _ => 0,
280 }
281 }
282
283 fn can_open(&self) -> bool {
284 match &self.token {
285 InlineToken::Delimiter { can_open, .. } => *can_open,
286 _ => false,
287 }
288 }
289
290 fn can_close(&self) -> bool {
291 match &self.token {
292 InlineToken::Delimiter { can_close, .. } => *can_close,
293 _ => false,
294 }
295 }
296
297 fn consume(&mut self, amount: usize) {
298 if let InlineToken::Delimiter { len, .. } = &mut self.token {
299 *len = len.saturating_sub(amount);
300 }
301 }
302}
303
304pub struct MarkdownStreamer {
305 active_fence: Option<(char, usize, usize)>, code_lang: String,
308
309 inline_code: InlineCodeState,
311
312 in_math_block: bool,
314 math_buffer: String,
315
316 in_table: bool,
318 table_header_printed: bool,
319
320 highlighter: Option<HighlightLines<'static>>,
322 line_buffer: String,
323
324 margin: usize,
326 blockquote_depth: usize,
327 list_context: ListContext,
328 pending_newline: bool,
329
330 manual_width: Option<usize>,
332
333 scratch_buffer: String,
335}
336
337impl Default for MarkdownStreamer {
338 fn default() -> Self {
339 Self::new()
340 }
341}
342
343impl MarkdownStreamer {
344 pub fn new() -> Self {
345 Self {
346 active_fence: None,
347 code_lang: "bash".to_string(),
348 inline_code: InlineCodeState::new(),
349 in_math_block: false,
350 math_buffer: String::new(),
351 in_table: false,
352 table_header_printed: false,
353 highlighter: None,
354 line_buffer: String::new(),
355 margin: 2,
356 blockquote_depth: 0,
357 list_context: ListContext::new(),
358 pending_newline: false,
359 manual_width: None,
360 scratch_buffer: String::with_capacity(1024),
361 }
362 }
363
364 pub fn set_width(&mut self, width: usize) {
366 self.manual_width = Some(width);
367 }
368
369 pub fn set_margin(&mut self, margin: usize) {
371 self.margin = margin;
372 }
373
374 fn get_width(&self) -> usize {
375 self.manual_width
376 .unwrap_or_else(crate::console::get_terminal_width)
377 }
378
379 fn visible_width(&self, text: &str) -> usize {
380 UnicodeWidthStr::width(RE_INVISIBLE.replace_all(text, "").as_ref())
381 }
382
383 pub fn print_chunk<W: Write>(&mut self, writer: &mut W, text: &str) -> io::Result<()> {
385 self.line_buffer.push_str(text);
386 while let Some(pos) = self.line_buffer.find('\n') {
387 let line = self.line_buffer[..pos + 1].to_string();
388 self.line_buffer.drain(..pos + 1);
389 self.process_line(writer, &line)?;
390 }
391 Ok(())
392 }
393
394 pub fn flush<W: Write>(&mut self, writer: &mut W) -> io::Result<()> {
396 if !self.line_buffer.is_empty() {
397 let line = std::mem::take(&mut self.line_buffer);
398 self.process_line(writer, &line)?;
399 }
400
401 self.flush_pending_inline(writer)?;
402 self.commit_newline(writer)?;
403 writer.flush()
404 }
405
406 fn commit_newline<W: Write>(&mut self, writer: &mut W) -> io::Result<()> {
407 if self.pending_newline {
408 queue!(writer, Print("\n"))?;
409 self.pending_newline = false;
410 }
411 Ok(())
412 }
413
414 fn flush_pending_inline<W: Write>(&mut self, writer: &mut W) -> io::Result<()> {
415 if let Some((ticks, buffer)) = self.inline_code.flush_incomplete() {
416 queue!(writer, Print("`".repeat(ticks)))?;
417 queue!(writer, Print(&buffer))?;
418 self.inline_code.reset();
419 }
420 Ok(())
421 }
422
423 fn process_line<W: Write>(&mut self, w: &mut W, raw_line: &str) -> io::Result<()> {
425 let expanded = self.expand_tabs(raw_line);
426 let trimmed = expanded.trim_end();
427
428 if self.try_handle_fence(w, &expanded, trimmed)? {
430 return Ok(());
431 }
432 if self.try_handle_math(w, trimmed)? {
433 return Ok(());
434 }
435 if self.try_handle_table(w, trimmed)? {
436 return Ok(());
437 }
438
439 let mut content = expanded.as_str();
441 self.blockquote_depth = 0;
442 while let Some(caps) = RE_BLOCKQUOTE.captures(content) {
443 self.blockquote_depth += 1;
444 content = caps.get(2).map_or("", |m| m.as_str());
445 }
446
447 let mut prefix = " ".repeat(self.margin);
448 if self.blockquote_depth > 0 {
449 prefix.push_str("\x1b[38;5;240m");
450 for _ in 0..self.blockquote_depth {
451 prefix.push_str("│ ");
452 }
453 prefix.push_str(STYLE_RESET);
454 }
455
456 let term_width = self.get_width();
457 let prefix_width = self.margin + (self.blockquote_depth * 2);
458 let avail_width = term_width.saturating_sub(prefix_width + self.margin);
459
460 let clean = content.trim_end();
464 if self.try_handle_header(w, clean, &prefix, avail_width)? {
465 return Ok(());
466 }
467 if self.try_handle_hr(w, clean, &prefix, avail_width)? {
468 return Ok(());
469 }
470 if self.try_handle_list(w, clean, &prefix, avail_width)? {
471 return Ok(());
472 }
473
474 self.render_standard_text(w, content, &prefix, avail_width)
476 }
477
478 fn try_handle_fence<W: Write>(
481 &mut self,
482 w: &mut W,
483 full: &str,
484 trimmed: &str,
485 ) -> io::Result<bool> {
486 let match_data = RE_CODE_FENCE.captures(trimmed);
487
488 if let Some((f_char, min_len, _)) = self.active_fence {
490 if let Some(caps) = &match_data {
491 let fence = &caps[2];
492 if fence.starts_with(f_char) && fence.len() >= min_len && caps[3].trim().is_empty()
493 {
494 self.active_fence = None;
495 self.commit_newline(w)?;
496 queue!(w, ResetColor)?;
497 self.pending_newline = true;
498 return Ok(true);
499 }
500 }
501 self.render_code_line(w, full)?;
502 return Ok(true);
503 }
504
505 if let Some(caps) = match_data {
507 let fence = &caps[2];
508 let indent_len = caps[1].len();
509 let info = caps[3].trim();
510 if let Some(f_char) = fence.chars().next()
511 && (f_char != '`' || !info.contains('`'))
512 {
513 self.flush_pending_inline(w)?;
514 self.commit_newline(w)?;
515 self.list_context.pop_to_indent(indent_len);
516 self.active_fence = Some((f_char, fence.len(), indent_len));
517 let lang = info.split_whitespace().next().unwrap_or("bash");
518 self.code_lang = lang.to_string();
519 self.start_highlighter(&self.code_lang.clone());
520 return Ok(true);
521 }
522 }
523 Ok(false)
524 }
525
526 fn try_handle_math<W: Write>(&mut self, w: &mut W, trimmed: &str) -> io::Result<bool> {
527 if RE_MATH_BLOCK.is_match(trimmed) {
528 if self.in_math_block {
529 self.in_math_block = false;
530 let converted = unicodeit::replace(&self.math_buffer);
531 let p_width = self.margin + (self.blockquote_depth * 2);
532 let avail = self.get_width().saturating_sub(p_width + self.margin);
533 let padding = avail.saturating_sub(self.visible_width(&converted)) / 2;
534
535 self.commit_newline(w)?;
536 queue!(
537 w,
538 Print(" ".repeat(self.margin + padding)),
539 SetForegroundColor(Color::Cyan),
540 SetAttribute(Attribute::Italic),
541 Print(converted),
542 ResetColor,
543 SetAttribute(Attribute::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 SetForegroundColor(Color::Yellow),
686 Print(disp_bullet),
687 Print(separator),
688 ResetColor
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 = prefix.to_string();
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 self.scratch_buffer.push_str(STYLE_RESET_BG);
979
980 let content_width = self.visible_width(line_content);
982
983 if content_width <= avail_width {
984 let pad = avail_width.saturating_sub(content_width);
986 queue!(
987 w,
988 Print(&prefix),
989 SetBackgroundColor(Color::Rgb {
990 r: 30,
991 g: 30,
992 b: 30
993 }),
994 Print(&self.scratch_buffer),
995 Print(" ".repeat(pad)),
996 ResetColor
997 )?;
998 } else {
999 let wrapped_lines = self.wrap_ansi(&self.scratch_buffer, avail_width);
1001
1002 if wrapped_lines.is_empty() {
1003 queue!(
1004 w,
1005 Print(&prefix),
1006 SetBackgroundColor(Color::Rgb {
1007 r: 30,
1008 g: 30,
1009 b: 30
1010 }),
1011 Print(" ".repeat(avail_width)),
1012 ResetColor
1013 )?;
1014 } else {
1015 for (i, line) in wrapped_lines.iter().enumerate() {
1016 if i > 0 {
1017 queue!(w, Print("\n"))?;
1018 }
1019 let vis_len = self.visible_width(line);
1020 let pad = avail_width.saturating_sub(vis_len);
1021
1022 queue!(
1023 w,
1024 Print(&prefix),
1025 SetBackgroundColor(Color::Rgb {
1026 r: 30,
1027 g: 30,
1028 b: 30
1029 }),
1030 Print(line),
1031 Print(" ".repeat(pad)),
1032 ResetColor
1033 )?;
1034 }
1035 }
1036 }
1037 self.pending_newline = true;
1038 Ok(())
1039 }
1040
1041 fn render_stream_table_row<W: Write>(&mut self, w: &mut W, row_str: &str) -> io::Result<()> {
1042 self.commit_newline(w)?;
1043 let term_width = self.get_width();
1044 let cells: Vec<&str> = row_str.trim().trim_matches('|').split('|').collect();
1045 if cells.is_empty() {
1046 return Ok(());
1047 }
1048
1049 let prefix_width = self.margin + (self.blockquote_depth * 2);
1050 let cell_overhead = (cells.len() * 3).saturating_sub(1);
1051 let avail = term_width.saturating_sub(prefix_width + self.margin + cell_overhead);
1052 if avail == 0 {
1053 return Ok(());
1054 }
1055 let base_w = avail / cells.len();
1056 let rem = avail % cells.len();
1057
1058 let bg = if !self.table_header_printed {
1059 Color::Rgb {
1060 r: 60,
1061 g: 60,
1062 b: 80,
1063 }
1064 } else {
1065 Color::Rgb {
1066 r: 30,
1067 g: 30,
1068 b: 30,
1069 }
1070 };
1071 let mut wrapped_cells = Vec::new();
1072 let mut max_h = 1;
1073
1074 for (i, cell) in cells.iter().enumerate() {
1075 let width = std::cmp::max(
1076 1,
1077 if i == cells.len() - 1 {
1078 base_w + rem
1079 } else {
1080 base_w
1081 },
1082 );
1083 self.scratch_buffer.clear();
1084 if !self.table_header_printed {
1085 self.scratch_buffer.push_str("\x1b[1;33m");
1086 }
1087 self.render_inline(
1088 cell.trim(),
1089 Some(bg),
1090 if !self.table_header_printed {
1091 Some("\x1b[1;33m")
1092 } else {
1093 None
1094 },
1095 );
1096 if !self.table_header_printed {
1097 self.scratch_buffer.push_str("\x1b[0m");
1098 }
1099
1100 let lines = self.wrap_ansi(&self.scratch_buffer, width);
1101 if lines.len() > max_h {
1102 max_h = lines.len();
1103 }
1104 wrapped_cells.push((lines, width));
1105 }
1106
1107 let mut prefix = " ".repeat(self.margin);
1108 if self.blockquote_depth > 0 {
1109 prefix.push_str("\x1b[38;5;240m");
1110 for _ in 0..self.blockquote_depth {
1111 prefix.push_str("│ ");
1112 }
1113 prefix.push_str(STYLE_RESET);
1114 }
1115
1116 for i in 0..max_h {
1117 if i > 0 {
1118 queue!(w, Print("\n"))?;
1119 }
1120 queue!(w, Print(&prefix))?;
1121 for (col, (lines, width)) in wrapped_cells.iter().enumerate() {
1122 let text = lines.get(i).map(|s| s.as_str()).unwrap_or("");
1123 let pad = width.saturating_sub(self.visible_width(text));
1124 queue!(
1125 w,
1126 SetBackgroundColor(bg),
1127 Print(" "),
1128 Print(text),
1129 SetBackgroundColor(bg),
1130 Print(" ".repeat(pad + 1)),
1131 ResetColor
1132 )?;
1133 if col < cells.len() - 1 {
1134 queue!(
1135 w,
1136 SetBackgroundColor(bg),
1137 SetForegroundColor(Color::White),
1138 Print("│"),
1139 ResetColor
1140 )?;
1141 }
1142 }
1143 }
1144 self.pending_newline = true;
1145 self.table_header_printed = true;
1146 Ok(())
1147 }
1148
1149 pub fn render_inline(&mut self, text: &str, def_bg: Option<Color>, restore_fg: Option<&str>) {
1150 static RE_AUTOLINK: LazyLock<Regex> = LazyLock::new(|| {
1152 Regex::new(r"<([a-zA-Z][a-zA-Z0-9+.-]{1,31}:[^<> \x00-\x1f]+)>").unwrap()
1153 });
1154
1155 let text_autolinked = RE_AUTOLINK.replace_all(text, |c: ®ex::Captures| {
1156 let url = &c[1];
1157 format!("\x1b]8;;{}\x1b\\{}\x1b]8;;\x1b\\", url, url)
1158 });
1159
1160 let text_linked = RE_LINK.replace_all(&text_autolinked, |c: ®ex::Captures| {
1162 format!(
1163 "\x1b]8;;{}\x1b\\\x1b[33;4m{}\x1b[24;39m\x1b]8;;\x1b\\",
1164 &c[2], &c[1]
1165 )
1166 });
1167
1168 let mut parts: Vec<InlinePart> = Vec::new();
1169 let caps_iter = RE_TOKENIZER.captures_iter(&text_linked);
1170 let tokens_raw: Vec<&str> = caps_iter.map(|c| c.get(1).unwrap().as_str()).collect();
1171
1172 for (i, tok) in tokens_raw.iter().enumerate() {
1174 if self.inline_code.is_active() {
1175 if tok.starts_with('`') {
1176 if let Some(content) = self.inline_code.feed(tok) {
1177 let formatted =
1178 self.format_inline_code_content(&content, def_bg, restore_fg);
1179 parts.push(InlinePart::text(formatted));
1180 }
1181 } else {
1182 self.inline_code.feed(tok);
1183 }
1184 continue;
1185 }
1186
1187 if tok.starts_with('`') {
1188 self.inline_code.open(tok.len());
1189 continue;
1190 }
1191
1192 if tok.starts_with('\\') && tok.len() > 1 {
1193 parts.push(InlinePart::text(tok[1..].to_string()));
1194 continue;
1195 }
1196
1197 if tok.starts_with('$') && tok.ends_with('$') && tok.len() > 1 {
1198 parts.push(InlinePart::text(unicodeit::replace(&tok[1..tok.len() - 1])));
1199 continue;
1200 }
1201
1202 if let Some(c) = tok.chars().next()
1203 && (c == '*' || c == '_' || c == '~')
1204 {
1205 let prev_char = if i > 0 {
1206 tokens_raw[i - 1].chars().last().unwrap_or(' ')
1207 } else {
1208 ' '
1209 };
1210 let next_char = if i + 1 < tokens_raw.len() {
1211 tokens_raw[i + 1].chars().next().unwrap_or(' ')
1212 } else {
1213 ' '
1214 };
1215
1216 let is_ws_next = next_char.is_whitespace();
1218 let is_ws_prev = prev_char.is_whitespace();
1219 let is_punct_next = !next_char.is_alphanumeric() && !is_ws_next;
1220 let is_punct_prev = !prev_char.is_alphanumeric() && !is_ws_prev;
1221 let left_flanking =
1222 !is_ws_next && (!is_punct_next || (is_ws_prev || is_punct_prev));
1223 let right_flanking =
1224 !is_ws_prev && (!is_punct_prev || (is_ws_next || is_punct_next));
1225
1226 let (can_open, can_close) = if c == '_' {
1227 (
1228 left_flanking && (!right_flanking || is_punct_prev),
1229 right_flanking && (!left_flanking || is_punct_next),
1230 )
1231 } else {
1232 (left_flanking, right_flanking)
1233 };
1234
1235 parts.push(InlinePart::delimiter(c, tok.len(), can_open, can_close));
1236 } else {
1237 parts.push(InlinePart::text(tok.to_string()));
1238 }
1239 }
1240
1241 self.resolve_delimiters(&mut parts);
1243
1244 for part in parts {
1246 for s in &part.pre_style {
1247 self.scratch_buffer.push_str(s);
1248 }
1249 self.scratch_buffer.push_str(&part.content());
1250 for s in &part.post_style {
1251 self.scratch_buffer.push_str(s);
1252 }
1253 }
1254 }
1255
1256 fn resolve_delimiters(&self, parts: &mut [InlinePart]) {
1257 let mut stack: Vec<usize> = Vec::new();
1258
1259 for i in 0..parts.len() {
1260 if !parts[i].is_delim() {
1261 continue;
1262 }
1263
1264 if parts[i].can_close() {
1265 let mut stack_idx = stack.len();
1266 while stack_idx > 0 {
1267 let open_pos = stack_idx - 1;
1268 let open_idx = stack[open_pos];
1269
1270 if parts[open_idx].delim_char() == parts[i].delim_char()
1271 && parts[open_idx].can_open()
1272 {
1273 if (parts[open_idx].can_open() && parts[open_idx].can_close())
1275 || (parts[i].can_open() && parts[i].can_close())
1276 {
1277 let sum = parts[open_idx].delim_len() + parts[i].delim_len();
1278 if sum.is_multiple_of(3)
1279 && (!parts[open_idx].delim_len().is_multiple_of(3)
1280 || !parts[i].delim_len().is_multiple_of(3))
1281 {
1282 stack_idx -= 1;
1283 continue;
1284 }
1285 }
1286
1287 if open_idx + 1 == i {
1289 stack_idx -= 1;
1290 continue;
1291 }
1292
1293 let open_len = parts[open_idx].delim_len();
1295 let close_len = parts[i].delim_len();
1296 let use_len = if close_len == 3 && open_len == 3 {
1297 1
1298 } else if close_len >= 2 && open_len >= 2 {
1299 2
1300 } else {
1301 1
1302 };
1303
1304 let (style_on, style_off) = match (parts[open_idx].delim_char(), use_len) {
1305 ('~', _) => ("\x1b[9m", "\x1b[29m"),
1306 ('_', 1) => ("\x1b[4m", "\x1b[24m"),
1307 (_, 1) => ("\x1b[3m", "\x1b[23m"),
1308 (_, 2) => ("\x1b[1m", "\x1b[22m"),
1309 _ => ("", ""),
1310 };
1311
1312 if use_len == 1 {
1314 parts[open_idx].pre_style.push(style_on.to_string());
1315 parts[i].post_style.push(style_off.to_string());
1316 } else {
1317 parts[open_idx].post_style.push(style_on.to_string());
1318 parts[i].pre_style.push(style_off.to_string());
1319 }
1320
1321 parts[open_idx].consume(use_len);
1323 parts[i].consume(use_len);
1324
1325 if parts[open_idx].delim_len() == 0 {
1327 stack.remove(open_pos);
1328 stack_idx -= 1;
1329 }
1330
1331 if parts[i].delim_len() == 0 {
1332 break;
1333 }
1334 } else {
1335 stack_idx -= 1;
1336 }
1337 }
1338 }
1339
1340 if parts[i].delim_len() > 0 && parts[i].can_open() {
1341 stack.push(i);
1342 }
1343 }
1344 }
1345
1346 fn format_inline_code_content(
1347 &self,
1348 content: &str,
1349 def_bg: Option<Color>,
1350 restore_fg: Option<&str>,
1351 ) -> String {
1352 let mut out = String::new();
1353 let _ = write!(out, "{}{}", STYLE_INLINE_CODE, content);
1354 if let Some(Color::Rgb { r, g, b }) = def_bg {
1355 let _ = write!(out, "\x1b[48;2;{};{};{}m", r, g, b);
1356 } else {
1357 out.push_str(STYLE_RESET_BG);
1358 }
1359 out.push_str(restore_fg.unwrap_or(STYLE_RESET_FG));
1360 out
1361 }
1362
1363 fn expand_tabs(&self, line: &str) -> String {
1364 let mut expanded = String::with_capacity(line.len());
1365 let mut col = 0;
1366 for c in line.chars() {
1367 if c == '\t' {
1368 let n = 4 - (col % 4);
1369 expanded.push_str(&" ".repeat(n));
1370 col += n;
1371 } else {
1372 expanded.push(c);
1373 col += UnicodeWidthChar::width(c).unwrap_or(0);
1374 }
1375 }
1376 expanded
1377 }
1378
1379 fn clean_atx_header_text<'a>(&self, text: &'a str) -> &'a str {
1380 let mut end = text.len();
1382 let bytes = text.as_bytes();
1383 while end > 0 && bytes[end - 1] == b'#' {
1384 end -= 1;
1385 }
1386 if end > 0 && end < text.len() && bytes[end - 1] == b' ' {
1387 &text[..end - 1]
1388 } else if end == 0 {
1389 ""
1390 } else {
1391 &text[..end]
1392 }
1393 }
1394
1395 fn start_highlighter(&mut self, lang: &str) {
1396 let ss = &*SYNTAX_SET;
1397 let syntax = ss
1398 .find_syntax_by_token(lang)
1399 .unwrap_or_else(|| ss.find_syntax_plain_text());
1400 self.highlighter = Some(HighlightLines::new(syntax, &THEME));
1401 }
1402}