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(SyntaxSet::load_defaults_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> =
52 LazyLock::new(|| Regex::new(r"\[([^\]]+)\]\(([^\)]+)\)").unwrap());
53static RE_OSC8: LazyLock<Regex> = LazyLock::new(|| Regex::new(OSC8_PATTERN).unwrap());
54
55static RE_TOKENIZER: LazyLock<Regex> = LazyLock::new(|| {
56 Regex::new(&format!(
57 r"({}|{}|`+|\\[\s\S]|\$[^\$\s](?:[^\$\n]*?[^\$\s])?\$|~~|~|\*\*\*|___|\*\*|__|\*|_|\$|[^~*_`$\\\x1b]+)",
58 OSC8_PATTERN, ANSI_REGEX_PATTERN
59 ))
60 .unwrap()
61});
62
63static RE_SPLIT_ANSI: LazyLock<Regex> = LazyLock::new(|| {
64 let pattern = format!(
65 "({}|{}|\\s+|[^\\s\\x1b]+)",
66 OSC8_PATTERN, ANSI_REGEX_PATTERN
67 );
68 Regex::new(&pattern).unwrap()
69});
70static RE_ANSI_PARTS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\x1b\[([0-9;]*)m").unwrap());
71
72struct InlinePart {
75 content: String,
76 is_delim: bool,
77 char: char,
78 len: usize,
79 can_open: bool,
80 can_close: bool,
81 pre_style: Vec<String>,
82 post_style: Vec<String>,
83}
84
85impl InlinePart {
86 fn text(content: String) -> Self {
87 Self {
88 content,
89 is_delim: false,
90 char: '\0',
91 len: 0,
92 can_open: false,
93 can_close: false,
94 pre_style: vec![],
95 post_style: vec![],
96 }
97 }
98}
99
100pub struct MarkdownStreamer {
101 active_fence: Option<(char, usize, usize)>, code_lang: String,
104
105 inline_code_ticks: Option<usize>,
107 inline_code_buffer: String,
108
109 in_math_block: bool,
111 math_buffer: String,
112
113 in_table: bool,
115 table_header_printed: bool,
116
117 highlighter: Option<HighlightLines<'static>>,
119 line_buffer: String,
120
121 margin: usize,
123 blockquote_depth: usize,
124 list_stack: Vec<(usize, bool, usize, usize)>, manual_width: Option<usize>,
128
129 scratch_buffer: String,
131}
132
133impl Default for MarkdownStreamer {
134 fn default() -> Self {
135 Self::new()
136 }
137}
138
139impl MarkdownStreamer {
140 pub fn new() -> Self {
141 Self {
142 active_fence: None,
143 code_lang: "bash".to_string(),
144 inline_code_ticks: None,
145 inline_code_buffer: String::new(),
146 in_math_block: false,
147 math_buffer: String::new(),
148 in_table: false,
149 table_header_printed: false,
150 highlighter: None,
151 line_buffer: String::new(),
152 margin: 2,
153 blockquote_depth: 0,
154 list_stack: Vec::new(),
155 manual_width: None,
156 scratch_buffer: String::with_capacity(1024),
157 }
158 }
159
160 pub fn set_width(&mut self, width: usize) {
162 self.manual_width = Some(width);
163 }
164
165 pub fn set_margin(&mut self, margin: usize) {
167 self.margin = margin;
168 }
169
170 fn get_width(&self) -> usize {
171 self.manual_width
172 .unwrap_or_else(crate::console::get_terminal_width)
173 }
174
175 fn visible_width(&self, text: &str) -> usize {
176 UnicodeWidthStr::width(RE_INVISIBLE.replace_all(text, "").as_ref())
177 }
178
179 pub fn print_chunk<W: Write>(&mut self, writer: &mut W, text: &str) -> io::Result<()> {
181 self.line_buffer.push_str(text);
182 while let Some(pos) = self.line_buffer.find('\n') {
183 let line = self.line_buffer[..pos + 1].to_string();
184 self.line_buffer.drain(..pos + 1);
185 self.process_line(writer, &line)?;
186 }
187 Ok(())
188 }
189
190 pub fn flush<W: Write>(&mut self, writer: &mut W) -> io::Result<()> {
192 if !self.line_buffer.is_empty() {
193 let line = std::mem::take(&mut self.line_buffer);
194 self.process_line(writer, &line)?;
195 }
196
197 if let Some(ticks) = self.inline_code_ticks {
199 queue!(writer, Print("`".repeat(ticks)))?;
201 queue!(writer, Print(&self.inline_code_buffer))?;
203 self.inline_code_ticks = None;
204 self.inline_code_buffer.clear();
205 }
206
207 writer.flush()
208 }
209
210 fn process_line<W: Write>(&mut self, w: &mut W, raw_line: &str) -> io::Result<()> {
212 let expanded = raw_line.replace('\t', " ");
213 let trimmed = expanded.trim_end();
214
215 if self.try_handle_fence(w, &expanded, trimmed)? {
217 return Ok(());
218 }
219 if self.try_handle_math(w, trimmed)? {
220 return Ok(());
221 }
222 if self.try_handle_table(w, trimmed)? {
223 return Ok(());
224 }
225
226 let mut content = expanded.as_str();
228 self.blockquote_depth = 0;
229 while let Some(caps) = RE_BLOCKQUOTE.captures(content) {
230 self.blockquote_depth += 1;
231 content = caps.get(2).map_or("", |m| m.as_str());
232 }
233
234 let mut prefix = " ".repeat(self.margin);
235 if self.blockquote_depth > 0 {
236 prefix.push_str("\x1b[38;5;240m");
237 for _ in 0..self.blockquote_depth {
238 prefix.push_str("│ ");
239 }
240 prefix.push_str("\x1b[0m");
241 }
242
243 let term_width = self.get_width();
244 let prefix_width = self.margin + (self.blockquote_depth * 2);
245 let avail_width = term_width.saturating_sub(prefix_width + self.margin);
246
247 if self.inline_code_ticks.is_none() {
249 let clean = content.trim_end();
250 if self.try_handle_header(w, clean, &prefix, avail_width)? {
251 return Ok(());
252 }
253 if self.try_handle_list(w, clean, &prefix, avail_width)? {
254 return Ok(());
255 }
256 if self.try_handle_hr(w, clean, &prefix, avail_width)? {
257 return Ok(());
258 }
259 }
260
261 self.render_standard_text(w, content, &prefix, avail_width)
263 }
264
265 fn try_handle_fence<W: Write>(
268 &mut self,
269 w: &mut W,
270 full: &str,
271 trimmed: &str,
272 ) -> io::Result<bool> {
273 let match_data = RE_CODE_FENCE.captures(trimmed);
274
275 if let Some((f_char, min_len, _)) = self.active_fence {
277 if let Some(caps) = &match_data {
278 let fence = &caps[2];
279 if fence.starts_with(f_char) && fence.len() >= min_len && caps[3].trim().is_empty()
280 {
281 self.active_fence = None;
282 queue!(w, ResetColor, Print("\n"))?;
283 return Ok(true);
284 }
285 }
286 self.render_code_line(w, full)?;
287 return Ok(true);
288 }
289
290 if let Some(caps) = match_data {
292 let fence = &caps[2];
293 let indent_len = caps[1].len();
294 let info = caps[3].trim();
295 if let Some(f_char) = fence.chars().next()
296 && (f_char != '`' || !info.contains('`'))
297 {
298 while self
299 .list_stack
300 .last()
301 .is_some_and(|(d, _, _, _)| *d > indent_len)
302 {
303 self.list_stack.pop();
304 }
305 self.active_fence = Some((f_char, fence.len(), indent_len));
306 let lang = info.split_whitespace().next().unwrap_or("bash");
307 self.code_lang = lang.to_string();
308 self.start_highlighter(&self.code_lang.clone());
309 return Ok(true);
310 }
311 }
312 Ok(false)
313 }
314
315 fn try_handle_math<W: Write>(&mut self, w: &mut W, trimmed: &str) -> io::Result<bool> {
316 if RE_MATH_BLOCK.is_match(trimmed) {
317 if self.in_math_block {
318 self.in_math_block = false;
319 let converted = unicodeit::replace(&self.math_buffer);
320 let p_width = self.margin + (self.blockquote_depth * 2);
321 let avail = self.get_width().saturating_sub(p_width + self.margin);
322 let padding = avail.saturating_sub(self.visible_width(&converted)) / 2;
323
324 queue!(
325 w,
326 Print(" ".repeat(self.margin + padding)),
327 SetForegroundColor(Color::Cyan),
328 SetAttribute(Attribute::Italic),
329 Print(converted),
330 ResetColor,
331 SetAttribute(Attribute::Reset),
332 Print("\n")
333 )?;
334 self.math_buffer.clear();
335 } else {
336 self.reset_block_context();
337 self.in_math_block = true;
338 }
339 return Ok(true);
340 }
341 if self.in_math_block {
342 self.math_buffer.push_str(trimmed);
343 self.math_buffer.push(' ');
344 return Ok(true);
345 }
346 Ok(false)
347 }
348
349 fn try_handle_table<W: Write>(&mut self, w: &mut W, trimmed: &str) -> io::Result<bool> {
350 if self.in_table && RE_TABLE_SEP.is_match(trimmed) {
351 self.table_header_printed = true;
352 return Ok(true);
353 }
354 if RE_TABLE_ROW.is_match(trimmed) {
355 if !self.in_table {
356 self.reset_block_context();
357 self.in_table = true;
358 }
359 self.render_stream_table_row(w, trimmed)?;
360 return Ok(true);
361 }
362 self.in_table = false;
363 self.table_header_printed = false;
364 Ok(false)
365 }
366
367 fn try_handle_header<W: Write>(
368 &mut self,
369 w: &mut W,
370 clean: &str,
371 prefix: &str,
372 avail: usize,
373 ) -> io::Result<bool> {
374 if let Some(caps) = RE_HEADER.captures(clean) {
375 let level = caps.get(1).map_or(0, |m| m.len());
376 let text = caps.get(2).map_or("", |m| m.as_str());
377 self.reset_block_context();
378
379 queue!(w, Print(prefix))?;
380 if level <= 2 {
381 queue!(w, Print("\n"))?;
382 }
383
384 self.scratch_buffer.clear();
385 let style = match level {
386 1 => "\x1b[1m",
387 2 => "\x1b[1;94m",
388 3 => "\x1b[1;36m",
389 _ => "\x1b[1;33m",
390 };
391 self.render_inline(text, None, Some(style));
392
393 if level <= 2 {
394 let lines = self.wrap_ansi(&self.scratch_buffer, avail);
395 for line in lines {
396 let pad = avail.saturating_sub(self.visible_width(&line)) / 2;
397 queue!(
398 w,
399 Print(" ".repeat(pad)),
400 Print(format!("{}{}\x1b[0m", style, line)),
401 ResetColor,
402 Print("\n")
403 )?;
404 if level == 1 {
405 queue!(w, Print(prefix))?;
406 }
407 }
408 } else {
409 queue!(
410 w,
411 Print(style),
412 Print(&self.scratch_buffer),
413 Print("\x1b[0m\n")
414 )?;
415 }
416 return Ok(true);
417 }
418 Ok(false)
419 }
420
421 fn try_handle_list<W: Write>(
422 &mut self,
423 w: &mut W,
424 clean: &str,
425 prefix: &str,
426 avail: usize,
427 ) -> io::Result<bool> {
428 if let Some(caps) = RE_LIST.captures(clean) {
429 let indent = caps.get(1).map_or(0, |m| m.len());
430 let bullet = caps.get(2).map_or("-", |m| m.as_str());
431 let separator = caps.get(3).map_or(" ", |m| m.as_str());
432 let text = caps.get(4).map_or("", |m| m.as_str());
433
434 if bullet == "*" && text.trim_end().ends_with('*') && text.len() < 20 {
438 return Ok(false);
439 }
440
441 let is_ord = bullet.chars().any(|c| c.is_numeric());
442 let disp_bullet = if is_ord { bullet } else { "•" };
443 let marker_width = self.visible_width(disp_bullet) + separator.len();
444
445 let last_indent = self.list_stack.last().map(|(d, _, _, _)| *d).unwrap_or(0);
446 if self.list_stack.is_empty() || indent > last_indent {
447 self.list_stack.push((indent, is_ord, 0, marker_width));
448 } else if indent < last_indent {
449 while self
450 .list_stack
451 .last()
452 .is_some_and(|(d, _, _, _)| *d > indent)
453 {
454 self.list_stack.pop();
455 }
456 if self
457 .list_stack
458 .last()
459 .is_some_and(|(d, _, _, _)| *d != indent)
460 {
461 self.list_stack.push((indent, is_ord, 0, marker_width));
462 }
463 } else {
464 if let Some(last) = self.list_stack.last_mut() {
466 last.3 = marker_width;
467 }
468 }
469
470 let full_stack_width: usize = self.list_stack.iter().map(|(_, _, _, w)| *w).sum();
471 let parent_width = full_stack_width.saturating_sub(marker_width);
472
473 let hang_indent = " ".repeat(full_stack_width);
474 let content_width = avail.saturating_sub(full_stack_width);
475
476 queue!(
477 w,
478 Print(prefix),
479 Print(" ".repeat(parent_width)),
480 SetForegroundColor(Color::Yellow),
481 Print(disp_bullet),
482 Print(separator),
483 ResetColor
484 )?;
485
486 if let Some(fcaps) = RE_CODE_FENCE.captures(text) {
488 queue!(w, Print("\n"))?;
489
490 let fence_chars = &fcaps[2];
491 let info = fcaps[3].trim();
492
493 if let Some(f_char) = fence_chars.chars().next() {
494 self.active_fence = Some((f_char, fence_chars.len(), 0));
495
496 let lang = info.split_whitespace().next().unwrap_or("bash");
497 self.code_lang = lang.to_string();
498 self.start_highlighter(&self.code_lang.clone());
499 }
500 return Ok(true);
501 }
502
503 self.scratch_buffer.clear();
504 self.render_inline(text, None, None);
505 let lines = self.wrap_ansi(&self.scratch_buffer, content_width);
506
507 if lines.is_empty() {
508 queue!(w, Print("\n"))?;
509 } else if let Some(first) = lines.first() {
510 queue!(w, Print(first), ResetColor, Print("\n"))?;
511 }
512 for line in lines.iter().skip(1) {
513 queue!(
514 w,
515 Print(prefix),
516 Print(&hang_indent),
517 Print(line),
518 ResetColor,
519 Print("\n")
520 )?;
521 }
522 return Ok(true);
523 }
524 Ok(false)
525 }
526
527 fn try_handle_hr<W: Write>(
528 &mut self,
529 w: &mut W,
530 clean: &str,
531 prefix: &str,
532 avail: usize,
533 ) -> io::Result<bool> {
534 if RE_HR.is_match(clean) {
535 queue!(
536 w,
537 Print(prefix),
538 SetForegroundColor(Color::DarkGrey),
539 Print("─".repeat(avail)),
540 ResetColor,
541 Print("\n")
542 )?;
543 self.reset_block_context();
544 return Ok(true);
545 }
546 Ok(false)
547 }
548
549 fn render_standard_text<W: Write>(
550 &mut self,
551 w: &mut W,
552 content: &str,
553 prefix: &str,
554 avail: usize,
555 ) -> io::Result<()> {
556 let mut line_content = content.trim_end_matches(['\n', '\r']);
557 if line_content.trim().is_empty() && content.ends_with('\n') {
558 self.reset_block_context();
559 queue!(
560 w,
561 Print(if self.blockquote_depth > 0 {
562 prefix
563 } else {
564 ""
565 }),
566 Print("\n")
567 )?;
568 return Ok(());
569 }
570
571 if !line_content.is_empty() || self.inline_code_ticks.is_some() {
572 let mut eff_prefix = prefix.to_string();
573 if !self.list_stack.is_empty() {
574 let current_indent = line_content.chars().take_while(|c| *c == ' ').count();
575 if current_indent == 0 {
576 self.list_stack.clear();
577 } else {
578 while self
579 .list_stack
580 .last()
581 .is_some_and(|(d, _, _, _)| *d > current_indent)
582 {
583 self.list_stack.pop();
584 }
585 }
586
587 if !self.list_stack.is_empty() {
588 let structural_indent: usize =
589 self.list_stack.iter().map(|(_, _, _, w)| *w).sum();
590 eff_prefix.push_str(&" ".repeat(structural_indent));
591
592 let skip = current_indent.min(structural_indent);
595 line_content = &line_content[skip..];
596 }
597 }
598
599 self.scratch_buffer.clear();
600 self.render_inline(line_content, None, None);
601 if self.inline_code_ticks.is_some() {
602 self.inline_code_buffer.push(' ');
603 }
604
605 let lines = self.wrap_ansi(&self.scratch_buffer, avail);
606 for line in lines {
607 queue!(
608 w,
609 ResetColor,
610 SetAttribute(Attribute::Reset),
611 Print(&eff_prefix),
612 Print(&line),
613 ResetColor,
614 Print("\n")
615 )?;
616 }
617 }
618 Ok(())
619 }
620
621 fn reset_block_context(&mut self) {
622 self.list_stack.clear();
623 self.table_header_printed = false;
624 }
625
626 fn wrap_ansi(&self, text: &str, width: usize) -> Vec<String> {
627 let mut lines = Vec::new();
628 let mut current_line = String::new();
629 let mut current_len = 0;
630 let mut active_codes: Vec<String> = Vec::new();
631
632 for caps in RE_SPLIT_ANSI.captures_iter(text) {
633 let token = caps.get(1).unwrap().as_str();
634 if token.starts_with("\x1b") {
635 current_line.push_str(token);
636 self.update_ansi_state(&mut active_codes, token);
640 } else {
641 let mut token_str = token;
642 let mut token_len = UnicodeWidthStr::width(token_str);
643
644 while current_len + token_len > width && width > 0 {
645 if current_len == 0 {
646 let mut split_idx = 0;
648 let mut split_len = 0;
649 for (idx, c) in token_str.char_indices() {
650 let c_w = UnicodeWidthStr::width(c.to_string().as_str());
651 if split_len + c_w > width {
652 break;
653 }
654 split_idx = idx + c.len_utf8();
655 split_len += c_w;
656 }
657 if split_idx == 0 {
658 split_idx = token_str.chars().next().map_or(0, |c| c.len_utf8());
659 }
660 if split_idx == 0 {
661 break;
662 } current_line.push_str(&token_str[..split_idx]);
665 lines.push(current_line);
666 current_line = active_codes.join("");
667 token_str = &token_str[split_idx..];
668 token_len = UnicodeWidthStr::width(token_str);
669 current_len = 0;
670 } else if !token_str.trim().is_empty() {
671 lines.push(current_line);
672 current_line = active_codes.join("");
673 current_len = 0;
674 } else {
675 token_str = "";
676 token_len = 0;
677 }
678 }
679 if !token_str.is_empty() {
680 current_line.push_str(token_str);
681 current_len += token_len;
682 }
683 }
684 }
685 if !current_line.is_empty() {
686 lines.push(current_line);
687 }
688 lines
689 }
690
691 fn update_ansi_state(&self, state: &mut Vec<String>, code: &str) {
692 if RE_OSC8.is_match(code) {
693 return;
694 }
695 if let Some(caps) = RE_ANSI_PARTS.captures(code) {
696 let content = caps.get(1).map_or("", |m| m.as_str());
697 if content == "0" || content.is_empty() {
698 state.clear();
699 return;
700 }
701
702 let num: i32 = content
703 .split(';')
704 .next()
705 .unwrap_or("0")
706 .parse()
707 .unwrap_or(0);
708 let category = match num {
709 1 | 22 => "bold",
710 3 | 23 => "italic",
711 4 | 24 => "underline",
712 30..=39 | 90..=97 => "fg",
713 40..=49 | 100..=107 => "bg",
714 _ => "other",
715 };
716 if category != "other" {
717 state.retain(|exist| {
718 let e_num: i32 = RE_ANSI_PARTS
719 .captures(exist)
720 .and_then(|c| c.get(1))
721 .map_or("0", |m| m.as_str())
722 .split(';')
723 .next()
724 .unwrap_or("0")
725 .parse()
726 .unwrap_or(0);
727 let e_cat = match e_num {
728 1 | 22 => "bold",
729 3 | 23 => "italic",
730 4 | 24 => "underline",
731 30..=39 | 90..=97 => "fg",
732 40..=49 | 100..=107 => "bg",
733 _ => "other",
734 };
735 e_cat != category
736 });
737 }
738 state.push(code.to_string());
739 }
740 }
741
742 fn render_code_line<W: Write>(&mut self, w: &mut W, line: &str) -> io::Result<()> {
743 let raw_line = line.trim_end_matches(&['\r', '\n'][..]);
744
745 let fence_indent = self.active_fence.map(|(_, _, i)| i).unwrap_or(0);
746
747 let mut chars = raw_line.chars();
749 let mut skipped = 0;
750 while skipped < fence_indent {
751 let as_str = chars.as_str();
752 if as_str.starts_with(' ') {
753 chars.next();
754 skipped += 1;
755 } else {
756 break;
757 }
758 }
759 let line_content = chars.as_str();
760
761 let mut prefix = " ".repeat(self.margin);
762 if !self.list_stack.is_empty() {
763 let indent_width: usize = self.list_stack.iter().map(|(_, _, _, w)| *w).sum();
764 prefix.push_str(&" ".repeat(indent_width));
765 }
766
767 let avail_width = self.get_width().saturating_sub(prefix.len() + self.margin);
768
769 let mut spans = Vec::new();
770 if let Some(h) = &mut self.highlighter {
771 if let Ok(ranges) = h.highlight_line(line_content, &SYNTAX_SET) {
772 spans = ranges;
773 } else {
774 spans.push((syntect::highlighting::Style::default(), line_content));
775 }
776 } else {
777 spans.push((syntect::highlighting::Style::default(), line_content));
778 }
779
780 self.scratch_buffer.clear();
782 for (style, text) in spans {
783 let _ = write!(
784 self.scratch_buffer,
785 "\x1b[38;2;{};{};{}m{}",
786 style.foreground.r, style.foreground.g, style.foreground.b, text
787 );
788 }
789
790 let wrapped_lines = self.wrap_ansi(&self.scratch_buffer, avail_width);
792
793 if wrapped_lines.is_empty() {
795 queue!(
796 w,
797 Print(&prefix),
798 SetBackgroundColor(Color::Rgb {
799 r: 30,
800 g: 30,
801 b: 30
802 }),
803 Print(" ".repeat(avail_width)),
804 ResetColor,
805 Print("\n")
806 )?;
807 } else {
808 for line in wrapped_lines {
809 let vis_len = self.visible_width(&line);
810 let pad = avail_width.saturating_sub(vis_len);
811
812 queue!(
813 w,
814 Print(&prefix),
815 SetBackgroundColor(Color::Rgb {
816 r: 30,
817 g: 30,
818 b: 30
819 }),
820 Print(&line),
821 Print(" ".repeat(pad)), ResetColor,
823 Print("\n")
824 )?;
825 }
826 }
827
828 Ok(())
829 }
830
831 fn render_stream_table_row<W: Write>(&mut self, w: &mut W, row_str: &str) -> io::Result<()> {
832 let term_width = self.get_width();
833 let cells: Vec<&str> = row_str.trim().trim_matches('|').split('|').collect();
834 if cells.is_empty() {
835 return Ok(());
836 }
837
838 let prefix_width = self.margin + (self.blockquote_depth * 2);
839 let avail = term_width.saturating_sub(prefix_width + self.margin + 1 + (cells.len() * 3));
840 if avail == 0 {
841 return Ok(());
842 }
843 let base_w = avail / cells.len();
844 let rem = avail % cells.len();
845
846 let bg = if !self.table_header_printed {
847 Color::Rgb {
848 r: 60,
849 g: 60,
850 b: 80,
851 }
852 } else {
853 Color::Rgb {
854 r: 30,
855 g: 30,
856 b: 30,
857 }
858 };
859 let mut wrapped_cells = Vec::new();
860 let mut max_h = 1;
861
862 for (i, cell) in cells.iter().enumerate() {
863 let width = std::cmp::max(
864 1,
865 if i == cells.len() - 1 {
866 base_w + rem
867 } else {
868 base_w
869 },
870 );
871 self.scratch_buffer.clear();
872 if !self.table_header_printed {
873 self.scratch_buffer.push_str("\x1b[1;33m");
874 }
875 self.render_inline(
876 cell.trim(),
877 Some(bg),
878 if !self.table_header_printed {
879 Some("\x1b[1;33m")
880 } else {
881 None
882 },
883 );
884 if !self.table_header_printed {
885 self.scratch_buffer.push_str("\x1b[0m");
886 }
887
888 let lines = self.wrap_ansi(&self.scratch_buffer, width);
889 if lines.len() > max_h {
890 max_h = lines.len();
891 }
892 wrapped_cells.push((lines, width));
893 }
894
895 let mut prefix = " ".repeat(self.margin);
896 if self.blockquote_depth > 0 {
897 prefix.push_str("\x1b[38;5;240m");
898 for _ in 0..self.blockquote_depth {
899 prefix.push_str("│ ");
900 }
901 prefix.push_str("\x1b[0m");
902 }
903
904 for i in 0..max_h {
905 queue!(w, Print(&prefix))?;
906 for (col, (lines, width)) in wrapped_cells.iter().enumerate() {
907 let text = lines.get(i).map(|s| s.as_str()).unwrap_or("");
908 let pad = width.saturating_sub(self.visible_width(text));
909 queue!(
910 w,
911 SetBackgroundColor(bg),
912 Print(" "),
913 Print(text),
914 SetBackgroundColor(bg),
915 Print(" ".repeat(pad + 1)),
916 ResetColor
917 )?;
918 if col < cells.len() - 1 {
919 queue!(
920 w,
921 SetBackgroundColor(bg),
922 SetForegroundColor(Color::White),
923 Print("│"),
924 ResetColor
925 )?;
926 }
927 }
928 queue!(w, Print("\n"))?;
929 }
930 self.table_header_printed = true;
931 Ok(())
932 }
933
934 pub fn render_inline(&mut self, text: &str, def_bg: Option<Color>, restore_fg: Option<&str>) {
935 let text_linked = RE_LINK.replace_all(text, |c: ®ex::Captures| {
937 format!(
938 "\x1b]8;;{}\x1b\\\x1b[33;4m{}\x1b[24;39m\x1b]8;;\x1b\\",
939 &c[2], &c[1]
940 )
941 });
942
943 let mut parts: Vec<InlinePart> = Vec::new();
944 let caps_iter = RE_TOKENIZER.captures_iter(&text_linked);
945 let tokens_raw: Vec<&str> = caps_iter.map(|c| c.get(1).unwrap().as_str()).collect();
946
947 for (i, tok) in tokens_raw.iter().enumerate() {
949 if self.inline_code_ticks.is_some() {
950 if tok.starts_with('`') {
951 if let Some(n) = self.inline_code_ticks {
952 if n == tok.len() {
953 let formatted = self.format_inline_code(def_bg, restore_fg);
954 parts.push(InlinePart::text(formatted));
955 self.inline_code_ticks = None;
956 self.inline_code_buffer.clear();
957 } else {
958 self.inline_code_buffer.push_str(tok);
959 }
960 }
961 } else {
962 self.inline_code_buffer.push_str(tok);
963 }
964 continue;
965 }
966
967 if tok.starts_with('`') {
968 self.inline_code_ticks = Some(tok.len());
969 self.inline_code_buffer.clear();
970 continue;
971 }
972
973 if tok.starts_with('\\') && tok.len() > 1 {
974 parts.push(InlinePart::text(tok[1..].to_string()));
975 continue;
976 }
977
978 if tok.starts_with('$') && tok.ends_with('$') && tok.len() > 1 {
979 parts.push(InlinePart::text(unicodeit::replace(&tok[1..tok.len() - 1])));
980 continue;
981 }
982
983 if let Some(c) = tok.chars().next()
984 && (c == '*' || c == '_' || c == '~')
985 {
986 let prev_char = if i > 0 {
987 tokens_raw[i - 1].chars().last().unwrap_or(' ')
988 } else {
989 ' '
990 };
991 let next_char = if i + 1 < tokens_raw.len() {
992 tokens_raw[i + 1].chars().next().unwrap_or(' ')
993 } else {
994 ' '
995 };
996
997 let is_ws_next = next_char.is_whitespace();
999 let is_ws_prev = prev_char.is_whitespace();
1000 let is_punct_next = !next_char.is_alphanumeric() && !is_ws_next;
1001 let is_punct_prev = !prev_char.is_alphanumeric() && !is_ws_prev;
1002 let left_flanking =
1003 !is_ws_next && (!is_punct_next || (is_ws_prev || is_punct_prev));
1004 let right_flanking =
1005 !is_ws_prev && (!is_punct_prev || (is_ws_next || is_punct_next));
1006
1007 let (can_open, can_close) = if c == '_' {
1008 (
1009 left_flanking && (!right_flanking || is_punct_prev),
1010 right_flanking && (!left_flanking || is_punct_next),
1011 )
1012 } else {
1013 (left_flanking, right_flanking)
1014 };
1015
1016 parts.push(InlinePart {
1017 content: tok.to_string(),
1018 is_delim: true,
1019 char: c,
1020 len: tok.len(),
1021 can_open,
1022 can_close,
1023 pre_style: vec![],
1024 post_style: vec![],
1025 });
1026 } else {
1027 parts.push(InlinePart::text(tok.to_string()));
1028 }
1029 }
1030
1031 self.resolve_delimiters(&mut parts);
1033
1034 for part in parts {
1036 for s in &part.pre_style {
1037 self.scratch_buffer.push_str(s);
1038 }
1039 self.scratch_buffer.push_str(&part.content);
1040 for s in &part.post_style {
1041 self.scratch_buffer.push_str(s);
1042 }
1043 }
1044 }
1045
1046 fn resolve_delimiters(&self, parts: &mut [InlinePart]) {
1047 let mut stack: Vec<usize> = Vec::new();
1048
1049 for i in 0..parts.len() {
1050 if !parts[i].is_delim {
1051 continue;
1052 }
1053
1054 if parts[i].can_close {
1055 let mut stack_idx = stack.len();
1057 while stack_idx > 0 {
1058 let open_pos = stack_idx - 1;
1059 let open_idx = stack[open_pos];
1060
1061 if parts[open_idx].char == parts[i].char && parts[open_idx].can_open {
1063 let use_len = if parts[i].len == 3 && parts[open_idx].len == 3 {
1067 1
1068 } else if parts[i].len >= 2 && parts[open_idx].len >= 2 {
1069 2
1070 } else {
1071 1
1072 };
1073
1074 let (style_on, style_off) = match (parts[open_idx].char, use_len) {
1075 ('~', _) => ("\x1b[9m", "\x1b[29m"),
1076 ('_', 1) => ("\x1b[4m", "\x1b[24m"),
1077 (_, 1) => ("\x1b[3m", "\x1b[23m"),
1078 (_, 2) => ("\x1b[1m", "\x1b[22m"),
1079 _ => ("", ""),
1080 };
1081
1082 let char_str = parts[open_idx].char.to_string();
1083
1084 if use_len == 1 {
1092 parts[open_idx].pre_style.push(style_on.to_string());
1094 parts[i].post_style.push(style_off.to_string());
1095 } else {
1096 parts[open_idx].post_style.push(style_on.to_string());
1098 parts[i].pre_style.push(style_off.to_string());
1099 }
1100
1101 parts[open_idx].len -= use_len;
1103 parts[i].len -= use_len;
1104 parts[open_idx].content = char_str.repeat(parts[open_idx].len);
1105 parts[i].content = char_str.repeat(parts[i].len);
1106
1107 if parts[open_idx].len == 0 {
1109 stack.remove(open_pos);
1110 stack_idx -= 1;
1113 } else {
1114 }
1119
1120 if parts[i].len == 0 {
1121 break;
1122 }
1123 } else {
1127 stack_idx -= 1;
1128 }
1129 }
1130 }
1131
1132 if parts[i].len > 0 && parts[i].can_open {
1133 stack.push(i);
1134 }
1135 }
1136 }
1137
1138 fn push_style(&mut self, active: bool, on: &str, off: &str) {
1139 self.scratch_buffer.push_str(if active { on } else { off });
1140 }
1141
1142 fn format_inline_code(&self, def_bg: Option<Color>, restore_fg: Option<&str>) -> String {
1143 let mut out = String::new();
1145 let norm = if self.inline_code_buffer.len() >= 2
1146 && self.inline_code_buffer.starts_with(' ')
1147 && self.inline_code_buffer.ends_with(' ')
1148 && !self.inline_code_buffer.trim().is_empty()
1149 {
1150 &self.inline_code_buffer[1..self.inline_code_buffer.len() - 1]
1151 } else {
1152 &self.inline_code_buffer
1153 };
1154
1155 let _ = write!(out, "\x1b[48;2;60;60;60m\x1b[38;2;255;255;255m{}", norm);
1156 if let Some(Color::Rgb { r, g, b }) = def_bg {
1157 let _ = write!(out, "\x1b[48;2;{};{};{}m", r, g, b);
1158 } else {
1159 out.push_str("\x1b[49m");
1160 }
1161 out.push_str(restore_fg.unwrap_or("\x1b[39m"));
1162 out
1163 }
1164
1165 fn start_highlighter(&mut self, lang: &str) {
1166 let ss = &*SYNTAX_SET;
1167 let syntax = ss
1168 .find_syntax_by_token(lang)
1169 .unwrap_or_else(|| ss.find_syntax_plain_text());
1170 self.highlighter = Some(HighlightLines::new(syntax, &THEME));
1171 }
1172}