aico/ui/
markdown_streamer.rs

1// This implementation is loosly based on https://github.com/day50-dev/Streamdown
2use 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
18// --- Static Resources ---
19static 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
29// Regexes
30static 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
43// Single pattern for "Invisible" content (ANSI codes + OSC8 links) used for width calculation
44static RE_INVISIBLE: LazyLock<Regex> =
45    LazyLock::new(|| Regex::new(&format!("({}|{})", OSC8_PATTERN, ANSI_REGEX_PATTERN)).unwrap());
46
47// --- ANSI & Links ---
48// Shared pattern for OSC 8 links: \x1b]8;; ... \x1b\
49const OSC8_PATTERN: &str = r"\x1b]8;;.*?\x1b\\";
50
51// Regex allows up to 2 levels of nested brackets/parentheses
52static 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
77// --- Helper Structs ---
78
79struct 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    // Code State
296    active_fence: Option<(char, usize, usize)>, // char, min_len, indent
297    code_lang: String,
298
299    // Inline Code State
300    inline_code: InlineCodeState,
301
302    // Math State
303    in_math_block: bool,
304    math_buffer: String,
305
306    // Table State
307    in_table: bool,
308    table_header_printed: bool,
309
310    // Parsing State
311    highlighter: Option<HighlightLines<'static>>,
312    line_buffer: String,
313
314    // Layout State
315    margin: usize,
316    blockquote_depth: usize,
317    list_context: ListContext,
318    pending_newline: bool,
319
320    // Configuration
321    manual_width: Option<usize>,
322
323    // Reusable buffer
324    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    /// Set a fixed width for rendering. If not set, terminal size is queried.
355    pub fn set_width(&mut self, width: usize) {
356        self.manual_width = Some(width);
357    }
358
359    /// Set the margin (default 2)
360    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    /// Main entry point: Process a chunk of text and write to the provided writer.
374    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    /// Flush remaining buffer (useful at end of stream).
385    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    // --- Pipeline Controller ---
414    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        // 1. Context-Specific Handlers (return true if consumed)
419        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        // 2. Global Layout Calculation (Blockquotes & Margins)
430        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        // 3. Block Start Handlers
451        // Note: Block handlers must now check for pending inline code and flush it
452        // if the block structure interrupts the inline span (Spec 6.1).
453        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        // 4. Standard Text / Lazy Continuation
465        self.render_standard_text(w, content, &prefix, avail_width)
466    }
467
468    // --- Specific Handlers ---
469
470    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        // Closing Fence
479        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        // Opening Fence
496        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                // Same level: update width in case marker size changed (e.g. 9. -> 10.)
659                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            // Check if the text portion looks like a code fence start (e.g., "```ruby")
679            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                    // To avoid double-indenting, we skip the source indentation that matches
772                    // the structural indentation we just applied via eff_prefix.
773                    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                // If it's an OSC8 link sequence, it has no visible width.
820                // update_ansi_state already ignores it for state tracking, but we must
821                // ensure we don't accidentally treat it as visible text below.
822                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                        // Force split long word
830                        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                        } // Empty string safety
846
847                        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        // Strip the fence's indentation from the content line
932        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        // 1. Build the full colored line in memory first
965        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        // 2. Wrap the colored string manually
975        let wrapped_lines = self.wrap_ansi(&self.scratch_buffer, avail_width);
976
977        // 3. Render each wrapped segment with consistent background
978        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)), // Fill remaining width with bg color
1008                    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        // Pre-process links
1126        let text_linked = RE_LINK.replace_all(text, |c: &regex::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        // Pass 1: Build basic tokens
1138        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                // Inline Flanking Logic
1182                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        // Pass 2: Delimiter Matching
1207        self.resolve_delimiters(&mut parts);
1208
1209        // Pass 3: Render
1210        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                        // Rule 9/10: Multiple of 3 Rule
1239                        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                        // Empty emphasis check
1253                        if open_idx + 1 == i {
1254                            stack_idx -= 1;
1255                            continue;
1256                        }
1257
1258                        // Determine consumption length
1259                        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                        // Apply styles
1278                        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                        // Consume tokens
1287                        parts[open_idx].consume(use_len);
1288                        parts[i].consume(use_len);
1289
1290                        // Stack Management
1291                        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}