Skip to main content

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