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