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