Codex Handbook
tui/src/markdown_render.rs 2771 lines
//! Low-level markdown event renderer for the TUI transcript.//!//! This module consumes `pulldown-cmark` events and emits styled `ratatui`//! lines, including table layout, width-aware wrapping, and local file-link//! display. It is the final rendering stage used by higher-level helpers in//! `markdown.rs`.//!//! This renderer intentionally treats local file links differently from normal web links. For//! local paths, the displayed text comes from the destination, not the markdown label, so//! transcripts show the real file target (including normalized location suffixes) and can shorten//! absolute paths relative to a known working directory.//!//! ## Table rendering pipeline//!//! When the parser emits `Tag::Table` .. `TagEnd::Table`, the writer//! accumulates header and body rows into a `TableState`, then hands it to//! `render_table_lines` which runs this pipeline://!//! 1. **Filter spillover rows** -- heuristic extraction of rows that are//!    artifacts of pulldown-cmark's lenient parsing.//! 2. **Normalize column counts** -- pad or truncate so every row matches the//!    alignment count.//! 3. **Compute column widths** -- allocate widths with content-aware//!    priority and iterative shrinking.//! 4. **Choose presentation** -- render theme-accented row-separated columns//!    while values remain scannable, otherwise transpose body rows//!    into key/value records separated by muted rules.//! 5. **Append spillover** -- extracted spillover rows rendered as plain text//!    after the table.//!//! ## Width allocation//!//! Columns are classified as Narrative (long prose), TokenHeavy (paths, URLs,//! or hashes), or Compact (short values such as counts and status labels).//! Token-heavy columns give up excess width before narrative columns so an//! oversized path does not collapse readable prose; compact values are//! preserved last. When compact values split, token-heavy values collapse into//! unusably short chunks, expansive cells form tall narrow strips across enough//! body rows, or even 3-char-wide columns cannot fit, body rows render as//! key/value records.use crate::markdown_text_merge::DecodedTextMerge;use crate::render::highlight::foreground_style_for_scopes;use crate::render::highlight::highlight_code_to_lines;use crate::render::line_utils::line_to_static;use crate::style::table_separator_style;use crate::terminal_hyperlinks::HyperlinkLine;use crate::terminal_hyperlinks::annotate_web_urls_in_line;use crate::terminal_hyperlinks::remap_wrapped_line;use crate::terminal_hyperlinks::visible_lines;use crate::terminal_hyperlinks::web_destination;use crate::wrapping::RtOptions;use crate::wrapping::adaptive_wrap_line;use crate::wrapping::word_wrap_line;use codex_utils_string::normalize_markdown_hash_location_suffix;use dirs::home_dir;use pulldown_cmark::Alignment;use pulldown_cmark::CodeBlockKind;use pulldown_cmark::CowStr;use pulldown_cmark::Event;use pulldown_cmark::HeadingLevel;use pulldown_cmark::Options;use pulldown_cmark::Parser;use pulldown_cmark::Tag;use pulldown_cmark::TagEnd;use ratatui::style::Style;use ratatui::style::Stylize;use ratatui::text::Line;use ratatui::text::Span;use ratatui::text::Text;use regex_lite::Regex;use std::ops::Range;use std::path::Path;use std::path::PathBuf;use std::sync::LazyLock;use unicode_width::UnicodeWidthChar;use unicode_width::UnicodeWidthStr;use url::Url;mod table_key_value;const TABLE_COLUMN_GAP: usize = 2;const TABLE_CELL_PADDING: usize = 1;const TABLE_HEADER_SEPARATOR_CHAR: char = '━';const TABLE_BODY_SEPARATOR_CHAR: char = '─';struct MarkdownStyles {    h1: Style,    h2: Style,    h3: Style,    h4: Style,    h5: Style,    h6: Style,    code: Style,    emphasis: Style,    strong: Style,    strikethrough: Style,    ordered_list_marker: Style,    unordered_list_marker: Style,    link: Style,    blockquote: Style,}impl Default for MarkdownStyles {    fn default() -> Self {        Self {            h1: Style::new().bold().underlined(),            h2: Style::new().bold(),            h3: Style::new().bold().italic(),            h4: Style::new().italic(),            h5: Style::new().italic(),            h6: Style::new().italic(),            code: Style::new().cyan(),            emphasis: Style::new().italic(),            strong: Style::new().bold(),            strikethrough: Style::new().crossed_out(),            ordered_list_marker: Style::new().light_blue(),            unordered_list_marker: Style::new(),            link: Style::new().cyan().underlined(),            blockquote: Style::new().green(),        }    }}#[derive(Clone, Debug)]struct IndentContext {    prefix: Vec<Span<'static>>,    marker: Option<Vec<Span<'static>>>,    is_list: bool,}impl IndentContext {    fn new(prefix: Vec<Span<'static>>, marker: Option<Vec<Span<'static>>>, is_list: bool) -> Self {        Self {            prefix,            marker,            is_list,        }    }}/// Styled content of a single cell in the table being parsed.////// A cell can contain multiple lines (hard breaks inside the cell) and rich inline spans (bold,/// code, links).  The `plain_text()` projection is used for column-width measurement; the styled/// `lines` are used for final rendering.#[derive(Clone, Debug, Default)]struct TableCell {    lines: Vec<HyperlinkLine>,}// TableCell mutators inlined — called per-span during table event parsing.impl TableCell {    #[inline]    fn ensure_line(&mut self) {        if self.lines.is_empty() {            self.lines.push(HyperlinkLine::new(Line::default()));        }    }    #[inline]    fn push_span(&mut self, span: Span<'static>) {        self.ensure_line();        if let Some(line) = self.lines.last_mut() {            line.line.push_span(span);        }    }    fn push_annotated(&mut self, mut appended: HyperlinkLine) {        self.ensure_line();        if let Some(line) = self.lines.last_mut() {            let shift = line.width();            line.line.spans.append(&mut appended.line.spans);            line.hyperlinks                .extend(appended.hyperlinks.into_iter().map(|mut link| {                    link.columns = link.columns.start + shift..link.columns.end + shift;                    link                }));        }    }    #[inline]    fn hard_break(&mut self) {        self.lines.push(HyperlinkLine::new(Line::default()));    }    fn plain_text(&self) -> String {        use std::fmt::Write;        let mut buf = String::new();        for (i, line) in self.lines.iter().enumerate() {            if i > 0 {                buf.push(' ');            }            for span in &line.line.spans {                let _ = write!(buf, "{}", span.content);            }        }        buf    }}/// Accumulates pulldown-cmark table events into a structured representation.////// `TableState` is created on `Tag::Table` and consumed on `TagEnd::Table`. Between those events,/// the Writer delegates cell content (text, code, html, breaks) into the `current_cell`, which is/// flushed into `current_row` on `TagEnd::TableCell`, then into `header`/`rows` on row/head end/// events.#[derive(Debug)]struct TableBodyRow {    cells: Vec<TableCell>,    has_table_pipe_syntax: bool,}#[derive(Debug)]struct TableState {    alignments: Vec<Alignment>,    header: Option<Vec<TableCell>>,    rows: Vec<TableBodyRow>,    current_row: Option<Vec<TableCell>>,    current_row_has_table_pipe_syntax: bool,    current_cell: Option<TableCell>,    in_header: bool,}impl TableState {    fn new(alignments: Vec<Alignment>) -> Self {        Self {            alignments,            header: None,            rows: Vec::new(),            current_row: None,            current_row_has_table_pipe_syntax: false,            current_cell: None,            in_header: false,        }    }}/// Rendered table output split by wrapping behavior.////// `table_lines` are prewrapped aligned rows or key/value records, except/// header-only tables may retain pipe fallback rows for normal wrapping./// `spillover_lines` are prose rows extracted from parser artifacts and should/// be routed through normal wrapping.struct RenderedTableLines {    table_lines: Vec<HyperlinkLine>,    table_lines_prewrapped: bool,    spillover_lines: Vec<HyperlinkLine>,}/// Classification of a table column for width-allocation priority.////// Token-heavy columns such as paths and URLs are allowed to wrap before prose becomes unreadable./// Compact columns such as counts or status words resist wrapping so their values stay scannable.#[derive(Clone, Copy, Debug, PartialEq, Eq)]enum TableColumnKind {    /// Long-form prose content (>= 4 avg words/cell or >= 28 avg char width).    Narrative,    /// Content dominated by long tokens, such as paths, URLs, and hashes.    TokenHeavy,    /// Short values, such as counts and status labels, that should resist wrapping.    Compact,}/// Per-column statistics used to drive the width-allocation algorithm.////// Collected in a single pass over the header and body rows before any/// shrinking decisions are made.#[derive(Clone, Debug)]struct TableColumnMetrics {    /// Widest cell content (display width) across header and all body rows.    max_width: usize,    /// Display width of the longest whitespace-delimited token in the header.    header_token_width: usize,    /// Display width of the longest whitespace-delimited token across body rows.    body_token_width: usize,    /// Classification derived from body token density and average cell content.    kind: TableColumnKind,}/// Render markdown with default wrapping behavior.////// Use this when the caller does not have a concrete render width yet (for/// example, snapshot tests or contexts that intentionally defer wrapping). If/// a viewport width is known, prefer [`render_markdown_text_with_width`] so/// table fallback and line wrapping decisions match the visible terminal.pub fn render_markdown_text(input: &str) -> Text<'static> {    render_markdown_text_with_width(input, /*width*/ None)}/// Render markdown constrained to a known terminal width.////// The renderer preserves columnar table structure while values remain/// scannable and falls back to key/value records when body rows cannot fit/// readably. Passing `None` keeps intrinsic line widths and disables/// width-driven wrapping in the markdown writer. Local file links render/// relative to the current process working directory.pub(crate) fn render_markdown_text_with_width(input: &str, width: Option<usize>) -> Text<'static> {    let cwd = std::env::current_dir().ok();    render_markdown_text_with_width_and_cwd(input, width, cwd.as_deref())}/// Render markdown with an explicit working directory for local file links.////// The `cwd` parameter controls how absolute local targets are shortened before display. Passing/// the session cwd keeps full renders, history cells, and streamed deltas visually aligned even/// when rendering happens away from the process cwd.pub(crate) fn render_markdown_text_with_width_and_cwd(    input: &str,    width: Option<usize>,    cwd: Option<&Path>,) -> Text<'static> {    Text::from(visible_lines(render_markdown_lines_with_width_and_cwd(        input, width, cwd,    )))}pub(crate) fn render_markdown_lines_with_width_and_cwd(    input: &str,    width: Option<usize>,    cwd: Option<&Path>,) -> Vec<HyperlinkLine> {    let mut options = Options::empty();    options.insert(Options::ENABLE_STRIKETHROUGH);    options.insert(Options::ENABLE_TABLES);    let parser = DecodedTextMerge::new(Parser::new_ext(input, options).into_offset_iter());    let mut w = Writer::new(input, parser, width, cwd);    w.run();    w.text}#[derive(Clone, Debug)]struct LinkState {    destination: String,    show_destination: bool,    /// Pre-rendered display text for local file links.    ///    /// When this is present, the markdown label is intentionally suppressed so the rendered    /// transcript always reflects the real target path.    local_target_display: Option<String>,}fn should_render_link_destination(dest_url: &str) -> bool {    !is_local_path_like_link(dest_url)}static COLON_LOCATION_SUFFIX_RE: LazyLock<Regex> =    LazyLock::new(        || match Regex::new(r":\d+(?::\d+)?(?:[-–]\d+(?::\d+)?)?$") {            Ok(regex) => regex,            Err(error) => panic!("invalid location suffix regex: {error}"),        },    );// Covered by load_location_suffix_regexes.static HASH_LOCATION_SUFFIX_RE: LazyLock<Regex> =    LazyLock::new(|| match Regex::new(r"^L\d+(?:C\d+)?(?:-L\d+(?:C\d+)?)?$") {        Ok(regex) => regex,        Err(error) => panic!("invalid hash location regex: {error}"),    });/// Stateful pulldown-cmark event consumer that builds styled `ratatui` output.////// Tracks inline style nesting, indent/blockquote context, list numbering,/// and an optional `TableState` for accumulating table events.  The/// `wrap_width` field enables width-aware line wrapping and table column/// allocation; when `None`, lines keep their intrinsic width.struct Writer<'a, I>where    I: Iterator<Item = (Event<'a>, Range<usize>)>,{    input: &'a str,    iter: I,    text: Vec<HyperlinkLine>,    styles: MarkdownStyles,    inline_styles: Vec<Style>,    indent_stack: Vec<IndentContext>,    list_indices: Vec<Option<u64>>,    list_needs_blank_before_next_item: Vec<bool>,    list_item_start_line_counts: Vec<usize>,    link: Option<LinkState>,    needs_newline: bool,    pending_marker_line: bool,    in_paragraph: bool,    in_code_block: bool,    code_block_lang: Option<String>,    code_block_buffer: String,    wrap_width: Option<usize>,    cwd: Option<PathBuf>,    line_ends_with_local_link_target: bool,    pending_local_link_soft_break: bool,    current_line_content: Option<HyperlinkLine>,    current_initial_indent: Vec<Span<'static>>,    current_subsequent_indent: Vec<Span<'static>>,    current_line_style: Style,    current_line_in_code_block: bool,    table_state: Option<TableState>,}impl<'a, I> Writer<'a, I>where    I: Iterator<Item = (Event<'a>, Range<usize>)>,{    fn new(input: &'a str, iter: I, wrap_width: Option<usize>, cwd: Option<&Path>) -> Self {        Self {            input,            iter,            text: Vec::new(),            styles: MarkdownStyles::default(),            inline_styles: Vec::new(),            indent_stack: Vec::new(),            list_indices: Vec::new(),            list_needs_blank_before_next_item: Vec::new(),            list_item_start_line_counts: Vec::new(),            link: None,            needs_newline: false,            pending_marker_line: false,            in_paragraph: false,            in_code_block: false,            code_block_lang: None,            code_block_buffer: String::new(),            wrap_width,            cwd: cwd.map(Path::to_path_buf),            line_ends_with_local_link_target: false,            pending_local_link_soft_break: false,            current_line_content: None,            current_initial_indent: Vec::new(),            current_subsequent_indent: Vec::new(),            current_line_style: Style::default(),            current_line_in_code_block: false,            table_state: None,        }    }    fn run(&mut self) {        while let Some((ev, range)) = self.iter.next() {            self.handle_event(ev, range);        }        self.flush_current_line();    }    fn handle_event(&mut self, event: Event<'a>, range: Range<usize>) {        self.prepare_for_event(&event);        match event {            Event::Start(tag) => self.start_tag(tag, range),            Event::End(tag) => self.end_tag(tag),            Event::Text(text) => self.text(text),            Event::Code(code) => self.code(code),            Event::SoftBreak => self.soft_break(),            Event::HardBreak => self.hard_break(),            Event::Rule => {                self.flush_current_line();                if !self.text.is_empty() {                    self.push_blank_line();                }                self.push_line(Line::from("———"));                self.needs_newline = true;            }            Event::Html(html) => self.html(html, /*inline*/ false),            Event::InlineHtml(html) => self.html(html, /*inline*/ true),            Event::FootnoteReference(_) => {}            Event::TaskListMarker(_) => {}        }    }    fn prepare_for_event(&mut self, event: &Event<'a>) {        if !self.pending_local_link_soft_break {            return;        }        // Local file links render from the destination at `TagEnd::Link`, so a Markdown soft break        // immediately before a descriptive `: ...` should stay inline instead of splitting the        // list item across two lines.        if matches!(event, Event::Text(text) if text.trim_start().starts_with(':')) {            self.pending_local_link_soft_break = false;            return;        }        self.pending_local_link_soft_break = false;        self.push_line(Line::default());    }    fn start_tag(&mut self, tag: Tag<'a>, range: Range<usize>) {        match tag {            Tag::Paragraph => self.start_paragraph(),            Tag::Heading { level, .. } => self.start_heading(level),            Tag::BlockQuote => self.start_blockquote(),            Tag::CodeBlock(kind) => {                let indent = match kind {                    CodeBlockKind::Fenced(_) => None,                    CodeBlockKind::Indented => Some(Span::from(" ".repeat(4))),                };                let lang = match kind {                    CodeBlockKind::Fenced(lang) => Some(lang.to_string()),                    CodeBlockKind::Indented => None,                };                self.start_codeblock(lang, indent)            }            Tag::List(start) => self.start_list(start),            Tag::Item => self.start_item(),            Tag::Emphasis => self.push_inline_style(self.styles.emphasis),            Tag::Strong => self.push_inline_style(self.styles.strong),            Tag::Strikethrough => self.push_inline_style(self.styles.strikethrough),            Tag::Link { dest_url, .. } => self.push_link(dest_url.to_string()),            Tag::Table(alignments) => self.start_table(alignments),            Tag::TableHead => self.start_table_head(),            Tag::TableRow => self.start_table_row(range),            Tag::TableCell => self.start_table_cell(),            Tag::HtmlBlock            | Tag::FootnoteDefinition(_)            | Tag::Image { .. }            | Tag::MetadataBlock(_) => {}        }    }    fn end_tag(&mut self, tag: TagEnd) {        match tag {            TagEnd::Paragraph => self.end_paragraph(),            TagEnd::Heading(_) => self.end_heading(),            TagEnd::BlockQuote => self.end_blockquote(),            TagEnd::CodeBlock => self.end_codeblock(),            TagEnd::List(_) => self.end_list(),            TagEnd::Item => {                self.flush_current_line();                let start_line_count = self.list_item_start_line_counts.pop().unwrap_or_default();                if self.text.len().saturating_sub(start_line_count) > 1                    && let Some(needs_blank) = self.list_needs_blank_before_next_item.last_mut()                {                    *needs_blank = true;                }                self.indent_stack.pop();                self.pending_marker_line = false;            }            TagEnd::Emphasis | TagEnd::Strong | TagEnd::Strikethrough => self.pop_inline_style(),            TagEnd::Link => self.pop_link(),            TagEnd::Table => self.end_table(),            TagEnd::TableHead => self.end_table_head(),            TagEnd::TableRow => self.end_table_row(),            TagEnd::TableCell => self.end_table_cell(),            TagEnd::HtmlBlock            | TagEnd::FootnoteDefinition            | TagEnd::Image            | TagEnd::MetadataBlock(_) => {}        }    }    fn start_paragraph(&mut self) {        if self.in_table_cell() {            return;        }        if self.needs_newline {            self.push_blank_line();        }        self.push_line(Line::default());        self.needs_newline = false;        self.in_paragraph = true;    }    fn end_paragraph(&mut self) {        if self.in_table_cell() {            return;        }        self.needs_newline = true;        self.in_paragraph = false;        self.pending_marker_line = false;    }    fn start_heading(&mut self, level: HeadingLevel) {        if self.in_table_cell() {            return;        }        if self.needs_newline {            self.push_line(Line::default());            self.needs_newline = false;        }        let heading_style = match level {            HeadingLevel::H1 => self.styles.h1,            HeadingLevel::H2 => self.styles.h2,            HeadingLevel::H3 => self.styles.h3,            HeadingLevel::H4 => self.styles.h4,            HeadingLevel::H5 => self.styles.h5,            HeadingLevel::H6 => self.styles.h6,        };        let content = format!("{} ", "#".repeat(level as usize));        self.push_line(Line::from(vec![Span::styled(content, heading_style)]));        self.push_inline_style(heading_style);        self.needs_newline = false;    }    fn end_heading(&mut self) {        if self.in_table_cell() {            return;        }        self.needs_newline = true;        self.pop_inline_style();    }    fn start_blockquote(&mut self) {        if self.in_table_cell() {            return;        }        if self.needs_newline {            self.push_blank_line();            self.needs_newline = false;        }        self.indent_stack.push(IndentContext::new(            vec![Span::from("> ")],            /*marker*/ None,            /*is_list*/ false,        ));    }    fn end_blockquote(&mut self) {        if self.in_table_cell() {            return;        }        self.indent_stack.pop();        self.needs_newline = true;    }    fn text(&mut self, text: CowStr<'a>) {        if self.suppressing_local_link_label() {            return;        }        self.line_ends_with_local_link_target = false;        if self.in_table_cell() {            self.push_text_to_table_cell(&text);            return;        }        if self.pending_marker_line {            self.push_line(Line::default());        }        self.pending_marker_line = false;        // When inside a fenced code block with a known language, accumulate        // text into the buffer for batch highlighting in end_codeblock().        // Append verbatim — pulldown-cmark text events already contain the        // original line breaks, so inserting separators would double them.        if self.in_code_block && self.code_block_lang.is_some() {            self.code_block_buffer.push_str(&text);            return;        }        if self.in_code_block && !self.needs_newline {            let has_content = self                .current_line_content                .as_ref()                .map(|line| !line.line.spans.is_empty())                .unwrap_or_else(|| {                    self.text                        .last()                        .map(|line| !line.line.spans.is_empty())                        .unwrap_or(false)                });            if has_content {                self.push_line(Line::default());            }        }        for (i, line) in text.lines().enumerate() {            if self.needs_newline {                self.push_line(Line::default());                self.needs_newline = false;            }            if i > 0 {                self.push_line(Line::default());            }            let content = line.to_string();            let style = self.inline_styles.last().copied().unwrap_or_default();            self.push_text_spans(&content, style);        }        self.needs_newline = false;    }    fn code(&mut self, code: CowStr<'a>) {        if self.suppressing_local_link_label() {            return;        }        self.line_ends_with_local_link_target = false;        if self.in_table_cell() {            self.push_span_to_table_cell(Span::from(code.into_string()).style(self.styles.code));            return;        }        if self.pending_marker_line {            self.push_line(Line::default());            self.pending_marker_line = false;        }        let span = Span::from(code.into_string()).style(self.styles.code);        self.push_span(span);    }    fn html(&mut self, html: CowStr<'a>, inline: bool) {        if self.suppressing_local_link_label() {            return;        }        self.line_ends_with_local_link_target = false;        if self.in_table_cell() {            let style = self.inline_styles.last().copied().unwrap_or_default();            for (i, line) in html.lines().enumerate() {                if i > 0 {                    self.push_table_cell_hard_break();                }                self.push_span_to_table_cell(Span::styled(line.to_string(), style));            }            if !inline {                self.push_table_cell_hard_break();            }            return;        }        self.pending_marker_line = false;        for (i, line) in html.lines().enumerate() {            if self.needs_newline {                self.push_line(Line::default());                self.needs_newline = false;            }            if i > 0 {                self.push_line(Line::default());            }            let style = self.inline_styles.last().copied().unwrap_or_default();            self.push_span(Span::styled(line.to_string(), style));        }        self.needs_newline = !inline;    }    fn hard_break(&mut self) {        if self.suppressing_local_link_label() {            return;        }        self.line_ends_with_local_link_target = false;        if self.in_table_cell() {            self.push_table_cell_hard_break();            return;        }        self.push_line(Line::default());    }    fn soft_break(&mut self) {        if self.suppressing_local_link_label() {            return;        }        if self.in_table_cell() {            let style = self.inline_styles.last().copied().unwrap_or_default();            self.push_span_to_table_cell(Span::styled(" ".to_string(), style));            return;        }        if self.line_ends_with_local_link_target {            self.pending_local_link_soft_break = true;            self.line_ends_with_local_link_target = false;            return;        }        self.line_ends_with_local_link_target = false;        self.push_line(Line::default());    }    fn start_list(&mut self, index: Option<u64>) {        if self.list_indices.is_empty() && self.needs_newline {            self.push_line(Line::default());        }        self.list_indices.push(index);        self.list_needs_blank_before_next_item.push(false);    }    fn end_list(&mut self) {        self.list_indices.pop();        self.list_needs_blank_before_next_item.pop();        self.needs_newline = true;    }    fn start_item(&mut self) {        if self            .list_needs_blank_before_next_item            .last_mut()            .map(std::mem::take)            .unwrap_or(false)        {            self.push_blank_line();        }        self.flush_current_line();        self.list_item_start_line_counts.push(self.text.len());        self.pending_marker_line = true;        let depth = self.list_indices.len();        let is_ordered = self            .list_indices            .last()            .map(Option::is_some)            .unwrap_or(false);        let width = depth * 4 - 3;        let marker = if let Some(last_index) = self.list_indices.last_mut() {            match last_index {                None => Some(vec![Span::styled(                    " ".repeat(width - 1) + "- ",                    self.styles.unordered_list_marker,                )]),                Some(index) => {                    *index += 1;                    Some(vec![Span::styled(                        format!("{:width$}. ", *index - 1),                        self.styles.ordered_list_marker,                    )])                }            }        } else {            None        };        let indent_prefix = if depth == 0 {            Vec::new()        } else {            let indent_len = if is_ordered { width + 2 } else { width + 1 };            vec![Span::from(" ".repeat(indent_len))]        };        self.indent_stack.push(IndentContext::new(            indent_prefix,            marker,            /*is_list*/ true,        ));        self.needs_newline = false;    }    fn start_codeblock(&mut self, lang: Option<String>, indent: Option<Span<'static>>) {        self.flush_current_line();        if !self.text.is_empty() {            self.push_blank_line();        }        self.in_code_block = true;        // Extract the language token from the info string.  CommonMark info        // strings can contain metadata after the language, separated by commas,        // spaces, or other delimiters (e.g. "rust,no_run", "rust title=demo").        // Take only the first token so the syntax lookup succeeds.        let lang = lang            .as_deref()            .and_then(|s| s.split([',', ' ', '\t']).next())            .filter(|s| !s.is_empty())            .map(std::string::ToString::to_string);        self.code_block_lang = lang;        self.code_block_buffer.clear();        self.indent_stack.push(IndentContext::new(            vec![indent.unwrap_or_default()],            /*marker*/ None,            /*is_list*/ false,        ));        self.needs_newline = true;    }    fn end_codeblock(&mut self) {        // If we buffered code for a known language, syntax-highlight it now.        if let Some(lang) = self.code_block_lang.take() {            let code = std::mem::take(&mut self.code_block_buffer);            if !code.is_empty() {                let highlighted = highlight_code_to_lines(&code, &lang);                for hl_line in highlighted {                    self.push_line(Line::default());                    for span in hl_line.spans {                        self.push_span(span);                    }                }            }        }        self.needs_newline = true;        self.in_code_block = false;        self.indent_stack.pop();    }    fn start_table(&mut self, alignments: Vec<Alignment>) {        self.flush_current_line();        if self.needs_newline {            self.push_blank_line();            self.needs_newline = false;        }        self.table_state = Some(TableState::new(alignments));    }    fn end_table(&mut self) {        let Some(table_state) = self.table_state.take() else {            return;        };        let RenderedTableLines {            table_lines,            table_lines_prewrapped,            spillover_lines,        } = self.render_table_lines(table_state);        let mut pending_marker_line = self.pending_marker_line;        for line in table_lines {            if table_lines_prewrapped {                self.push_prewrapped_line(line, pending_marker_line);            } else {                self.push_hyperlink_line(line);                self.flush_current_line();            }            pending_marker_line = false;        }        self.pending_marker_line = false;        for spillover_line in spillover_lines {            self.push_hyperlink_line(spillover_line);            self.flush_current_line();        }        self.needs_newline = true;    }    fn start_table_head(&mut self) {        if let Some(table_state) = self.table_state.as_mut() {            table_state.in_header = true;            table_state.current_row = Some(Vec::new());        }    }    fn end_table_head(&mut self) {        let Some(table_state) = self.table_state.as_mut() else {            return;        };        if let Some(current_cell) = table_state.current_cell.take() {            table_state                .current_row                .get_or_insert_with(Vec::new)                .push(current_cell);        }        if let Some(row) = table_state.current_row.take() {            table_state.header = Some(row);        }        table_state.in_header = false;    }    fn start_table_row(&mut self, source_range: Range<usize>) {        let has_table_pipe_syntax = self.has_table_row_boundary_pipe(source_range);        if let Some(table_state) = self.table_state.as_mut() {            table_state.current_row = Some(Vec::new());            table_state.current_row_has_table_pipe_syntax = has_table_pipe_syntax;        }    }    fn has_table_row_boundary_pipe(&self, source_range: Range<usize>) -> bool {        let Some(source) = self.input.get(source_range) else {            return false;        };        let source = source.trim();        source.starts_with('|') || source.ends_with('|')    }    fn end_table_row(&mut self) {        let Some(table_state) = self.table_state.as_mut() else {            return;        };        if let Some(current_cell) = table_state.current_cell.take() {            table_state                .current_row                .get_or_insert_with(Vec::new)                .push(current_cell);        }        let Some(row) = table_state.current_row.take() else {            return;        };        if table_state.in_header {            table_state.header = Some(row);        } else {            table_state.rows.push(TableBodyRow {                cells: row,                has_table_pipe_syntax: table_state.current_row_has_table_pipe_syntax,            });        }        table_state.current_row_has_table_pipe_syntax = false;    }    fn start_table_cell(&mut self) {        if let Some(table_state) = self.table_state.as_mut() {            table_state.current_cell = Some(TableCell::default());        }    }    fn end_table_cell(&mut self) {        let Some(table_state) = self.table_state.as_mut() else {            return;        };        if let Some(cell) = table_state.current_cell.take() {            table_state                .current_row                .get_or_insert_with(Vec::new)                .push(cell);        }    }    fn in_table_cell(&self) -> bool {        self.table_state            .as_ref()            .and_then(|table_state| table_state.current_cell.as_ref())            .is_some()    }    fn push_span_to_table_cell(&mut self, span: Span<'static>) {        if let Some(table_state) = self.table_state.as_mut()            && let Some(cell) = table_state.current_cell.as_mut()        {            cell.push_span(span);        }    }    fn push_table_cell_hard_break(&mut self) {        if let Some(table_state) = self.table_state.as_mut()            && let Some(cell) = table_state.current_cell.as_mut()        {            cell.hard_break();        }    }    fn push_text_to_table_cell(&mut self, text: &str) {        let style = self.inline_styles.last().copied().unwrap_or_default();        for (i, line) in text.lines().enumerate() {            if i > 0 {                self.push_table_cell_hard_break();            }            self.push_text_spans_to_table_cell(line, style);        }    }    fn push_text_spans_to_table_cell(&mut self, text: &str, style: Style) {        let span = Span::styled(text.to_string(), style);        let destination = self            .link            .as_ref()            .and_then(|link| web_destination(&link.destination));        let mut annotated = if let Some(destination) = destination {            let mut annotated = HyperlinkLine::new(Line::default());            annotated.push_span(span, Some(&destination));            annotated        } else if self.link.is_some() || self.in_code_block {            HyperlinkLine::new(Line::from(span))        } else {            annotate_web_urls_in_line(Line::from(span))        };        if let Some(table_state) = self.table_state.as_mut()            && let Some(cell) = table_state.current_cell.as_mut()        {            cell.push_annotated(std::mem::take(&mut annotated));        }    }    /// Convert a completed `TableState` into styled table `Line`s.    ///    /// Pipeline: filter spillover rows -> normalize column counts -> compute    /// column widths -> render aligned rows or key/value records when values    /// systemically lose token readability or expansive cells become tall    /// narrow strips. Spillover rows are appended as plain text after the    /// table.    ///    /// Falls back to key/value records when body rows cannot fit in the aligned    /// grid; header-only tables retain raw pipe output because they contain no    /// records to transpose.    fn render_table_lines(&self, mut table_state: TableState) -> RenderedTableLines {        let column_count = table_state.alignments.len();        if column_count == 0 {            return RenderedTableLines {                table_lines: Vec::new(),                table_lines_prewrapped: true,                spillover_lines: Vec::new(),            };        }        let mut spillover_rows: Vec<TableCell> = Vec::with_capacity(4);        let mut rows: Vec<Vec<TableCell>> = Vec::with_capacity(table_state.rows.len());        for (row_idx, row) in table_state.rows.iter().enumerate() {            let next_row = table_state.rows.get(row_idx + 1);            // pulldown-cmark accepts body rows without pipes, which can turn a following paragraph            // into a one-cell table row. For multi-column tables, treat those as spillover text            // rendered after the table.            if column_count > 1 && Self::is_spillover_row(row, next_row) {                if let Some(cell) = row.cells.first().cloned() {                    spillover_rows.push(cell);                }            } else {                rows.push(row.cells.clone());            }        }        let mut header = table_state            .header            .take()            .unwrap_or_else(|| vec![TableCell::default(); column_count]);        Self::normalize_row(&mut header, column_count);        for row in &mut rows {            Self::normalize_row(row, column_count);        }        let metrics = Self::collect_table_column_metrics(&header, &rows, column_count);        let available_width = self.available_table_width(column_count);        let widths =            self.compute_column_widths(&header, &rows, &table_state.alignments, available_width);        let spillover_lines: Vec<HyperlinkLine> = spillover_rows            .into_iter()            .flat_map(|spillover| spillover.lines)            .collect();        let header_style =            foreground_style_for_scopes(&["entity.name.type", "support.type", "variable"])                .unwrap_or(self.styles.strong)                .bold();        let separator_style = table_separator_style();        let Some(column_widths) = widths else {            if !rows.is_empty() {                return RenderedTableLines {                    table_lines: table_key_value::render_records(                        &header,                        &rows,                        &metrics,                        self.available_record_width(),                        header_style,                        separator_style,                    ),                    table_lines_prewrapped: true,                    spillover_lines,                };            }            return RenderedTableLines {                table_lines: self.render_table_pipe_fallback(                    &header,                    &rows,                    &table_state.alignments,                ),                table_lines_prewrapped: false,                spillover_lines,            };        };        if table_key_value::should_render_records(&rows, &column_widths, &metrics) {            return RenderedTableLines {                table_lines: table_key_value::render_records(                    &header,                    &rows,                    &metrics,                    self.available_record_width(),                    header_style,                    separator_style,                ),                table_lines_prewrapped: true,                spillover_lines,            };        }        let mut out = Vec::with_capacity(2 + rows.len() * 2);        out.extend(self.render_table_row(            &header,            &column_widths,            &table_state.alignments,            header_style,        ));        out.push(Self::render_table_separator(            &column_widths,            TABLE_HEADER_SEPARATOR_CHAR,            separator_style,        ));        for (row_idx, row) in rows.iter().enumerate() {            out.extend(self.render_table_row(                row,                &column_widths,                &table_state.alignments,                Style::default(),            ));            if row_idx + 1 < rows.len() {                out.push(Self::render_table_separator(                    &column_widths,                    TABLE_BODY_SEPARATOR_CHAR,                    separator_style,                ));            }        }        RenderedTableLines {            table_lines: out,            table_lines_prewrapped: true,            spillover_lines,        }    }    fn normalize_row(row: &mut Vec<TableCell>, column_count: usize) {        row.truncate(column_count);        row.resize(column_count, TableCell::default());    }    /// Subtract horizontal gutters and per-cell padding from the content budget.    fn available_table_width(&self, column_count: usize) -> Option<usize> {        self.wrap_width.map(|wrap_width| {            let prefix_width =                Self::spans_display_width(&self.prefix_spans(self.pending_marker_line));            let reserved = prefix_width                + (column_count.saturating_sub(1) * TABLE_COLUMN_GAP)                + (column_count * TABLE_CELL_PADDING * 2);            wrap_width.saturating_sub(reserved)        })    }    /// Return the full content budget for record fallback rendering.    fn available_record_width(&self) -> Option<usize> {        self.wrap_width.map(|wrap_width| {            let prefix_width =                Self::spans_display_width(&self.prefix_spans(self.pending_marker_line));            wrap_width.saturating_sub(prefix_width)        })    }    /// Allocate column widths for aligned, row-separated table rendering.    ///    /// Each column starts at its natural (max cell content) width, then columns    /// are iteratively shrunk one character at a time until the total fits within    /// `available_width`. Token-heavy columns surrender excess width before    /// narrative prose; compact columns are preserved last. Returns `None` when    /// even the minimum width (3 chars per column) cannot fit.    fn compute_column_widths(        &self,        header: &[TableCell],        rows: &[Vec<TableCell>],        alignments: &[Alignment],        available_width: Option<usize>,    ) -> Option<Vec<usize>> {        let min_column_width = 3usize;        let metrics = Self::collect_table_column_metrics(header, rows, alignments.len());        let mut widths: Vec<usize> = metrics            .iter()            .map(|col| col.max_width.max(min_column_width))            .collect();        let Some(max_width) = available_width else {            return Some(widths);        };        let minimum_total = alignments.len() * min_column_width;        if max_width < minimum_total {            return None;        }        let mut floors: Vec<usize> = metrics            .iter()            .map(|col| Self::preferred_column_floor(col, min_column_width))            .collect();        let mut floor_total: usize = floors.iter().sum();        if floor_total > max_width {            // Relax preferred floors in wrapping priority order until the hard width budget fits.            while floor_total > max_width {                let Some((idx, _)) = floors                    .iter()                    .enumerate()                    .filter(|(_, floor)| **floor > min_column_width)                    .min_by_key(|(idx, floor)| {                        (                            Self::column_shrink_priority(metrics[*idx].kind),                            usize::MAX.saturating_sub(**floor),                        )                    })                else {                    break;                };                floors[idx] -= 1;                floor_total -= 1;            }        }        let mut total_width: usize = widths.iter().sum();        while total_width > max_width {            let Some(idx) = Self::next_column_to_shrink(&widths, &floors, &metrics) else {                break;            };            widths[idx] -= 1;            total_width -= 1;        }        if total_width > max_width {            return None;        }        Some(widths)    }    fn collect_table_column_metrics(        header: &[TableCell],        rows: &[Vec<TableCell>],        column_count: usize,    ) -> Vec<TableColumnMetrics> {        let mut metrics = Vec::with_capacity(column_count);        for column in 0..column_count {            let header_cell = &header[column];            let header_plain = header_cell.plain_text();            let header_token_width = Self::longest_token_width(&header_plain);            let mut max_width = Self::cell_display_width(header_cell);            let mut body_token_width = 0usize;            let mut body_token_count = 0usize;            let mut long_body_token_count = 0usize;            let mut total_words = 0usize;            let mut total_cells = 0usize;            let mut total_cell_width = 0usize;            for row in rows {                let cell = &row[column];                max_width = max_width.max(Self::cell_display_width(cell));                let plain = cell.plain_text();                body_token_width = body_token_width.max(Self::longest_token_width(&plain));                let word_count = plain.split_whitespace().count();                if word_count > 0 {                    body_token_count += word_count;                    long_body_token_count += plain                        .split_whitespace()                        .filter(|token| token.width() >= 20)                        .count();                    total_words += word_count;                    total_cells += 1;                    total_cell_width += plain.width();                }            }            let avg_words_per_cell = if total_cells == 0 {                header_plain.split_whitespace().count() as f64            } else {                total_words as f64 / total_cells as f64            };            let avg_cell_width = if total_cells == 0 {                header_plain.width() as f64            } else {                total_cell_width as f64 / total_cells as f64            };            let kind = if long_body_token_count > 0                && long_body_token_count >= body_token_count.saturating_sub(long_body_token_count)            {                TableColumnKind::TokenHeavy            } else if avg_words_per_cell >= 4.0 || avg_cell_width >= 28.0 {                TableColumnKind::Narrative            } else {                TableColumnKind::Compact            };            metrics.push(TableColumnMetrics {                max_width,                header_token_width,                body_token_width,                kind,            });        }        metrics    }    /// Compute the preferred minimum width for a column before the shrink loop    /// starts reducing it further.    ///    /// Narrative and token-heavy columns retain a readable 16-cell soft floor.    /// Compact columns floor at the larger of the header and body token widths    /// (body capped at 16). The result is clamped to `[min_column_width, max_width]`.    fn preferred_column_floor(metrics: &TableColumnMetrics, min_column_width: usize) -> usize {        let token_target = match metrics.kind {            TableColumnKind::Narrative | TableColumnKind::TokenHeavy => 16,            TableColumnKind::Compact => metrics                .header_token_width                .max(metrics.body_token_width.min(16)),        };        token_target.max(min_column_width).min(metrics.max_width)    }    /// Pick the next column to shrink by one character during width allocation.    ///    /// Priority: TokenHeavy columns are shrunk before Narrative, then Compact.    /// Within the same kind, the column with the most slack above its floor is    /// chosen so similarly-shaped columns stay balanced.    fn next_column_to_shrink(        widths: &[usize],        floors: &[usize],        metrics: &[TableColumnMetrics],    ) -> Option<usize> {        widths            .iter()            .enumerate()            .filter(|(idx, width)| **width > floors[*idx])            .min_by_key(|(idx, width)| {                let slack = width.saturating_sub(floors[*idx]);                (                    Self::column_shrink_priority(metrics[*idx].kind),                    usize::MAX.saturating_sub(slack),                )            })            .map(|(idx, _)| idx)    }    fn column_shrink_priority(kind: TableColumnKind) -> usize {        match kind {            TableColumnKind::TokenHeavy => 0,            TableColumnKind::Narrative => 1,            TableColumnKind::Compact => 2,        }    }    fn render_table_separator(        column_widths: &[usize],        separator_char: char,        style: Style,    ) -> HyperlinkLine {        let segment_char = separator_char.to_string();        let gap = " ".repeat(TABLE_COLUMN_GAP);        let text = column_widths            .iter()            .map(|width| segment_char.repeat(*width + (TABLE_CELL_PADDING * 2)))            .collect::<Vec<_>>()            .join(&gap);        HyperlinkLine::new(Line::from(Span::styled(text, style)))    }    fn render_table_row(        &self,        row: &[TableCell],        column_widths: &[usize],        alignments: &[Alignment],        row_style: Style,    ) -> Vec<HyperlinkLine> {        let wrapped_cells: Vec<Vec<HyperlinkLine>> = row            .iter()            .zip(column_widths)            .map(|(cell, width)| self.wrap_cell(cell, *width))            .collect();        let row_height = wrapped_cells.iter().map(Vec::len).max().unwrap_or(1);        let mut out = Vec::with_capacity(row_height);        for row_line in 0..row_height {            let Some(last_visible_column) = wrapped_cells.iter().rposition(|lines| {                lines                    .get(row_line)                    .is_some_and(|line| Self::line_display_width(&line.line) > 0)            }) else {                out.push(HyperlinkLine::new(Line::default().style(row_style)));                continue;            };            let mut spans = Vec::new();            for (column, width) in column_widths                .iter()                .enumerate()                .take(last_visible_column + 1)            {                spans.push(Span::raw(" ".repeat(TABLE_CELL_PADDING)));                let mut line = wrapped_cells[column]                    .get(row_line)                    .cloned()                    .unwrap_or_default();                let line_width = Self::line_display_width(&line.line);                let remaining = width.saturating_sub(line_width);                let (left_padding, right_padding) = match alignments[column] {                    Alignment::Left | Alignment::None => (0, remaining),                    Alignment::Center => (remaining / 2, remaining - (remaining / 2)),                    Alignment::Right => (remaining, 0),                };                if left_padding > 0 {                    spans.push(Span::raw(" ".repeat(left_padding)));                }                spans.append(&mut line.line.spans);                let is_last_column = column == last_visible_column;                if right_padding > 0 && !is_last_column {                    spans.push(Span::raw(" ".repeat(right_padding)));                }                if !is_last_column {                    spans.push(Span::raw(" ".repeat(TABLE_CELL_PADDING)));                }                if !is_last_column {                    spans.push(Span::raw(" ".repeat(TABLE_COLUMN_GAP)));                }            }            let mut out_line = HyperlinkLine::new(Line::from(spans).style(row_style));            let mut column_start = 0usize;            for (column, width) in column_widths                .iter()                .enumerate()                .take(last_visible_column + 1)            {                column_start += TABLE_CELL_PADDING;                if let Some(line) = wrapped_cells[column].get(row_line) {                    let remaining = width.saturating_sub(Self::line_display_width(&line.line));                    let left_padding = match alignments[column] {                        Alignment::Left | Alignment::None => 0,                        Alignment::Center => remaining / 2,                        Alignment::Right => remaining,                    };                    out_line                        .hyperlinks                        .extend(line.hyperlinks.iter().cloned().map(|mut link| {                            link.columns = link.columns.start + column_start + left_padding                                ..link.columns.end + column_start + left_padding;                            link                        }));                }                column_start += *width + TABLE_CELL_PADDING + TABLE_COLUMN_GAP;            }            out.push(out_line);        }        out    }    /// Render a header-only table as raw pipe-delimited lines (`| A | B |`).    ///    /// Used when `compute_column_widths` returns `None` and there are no body    /// records to transpose. Pipe characters inside cell content are escaped    /// as `\|` so downstream parsers keep cell boundaries intact.    fn render_table_pipe_fallback(        &self,        header: &[TableCell],        rows: &[Vec<TableCell>],        alignments: &[Alignment],    ) -> Vec<HyperlinkLine> {        let mut out = Vec::new();        out.push(Self::row_to_pipe_line(header));        out.push(HyperlinkLine::new(Line::from(            Self::alignments_to_pipe_delimiter(alignments),        )));        out.extend(rows.iter().map(|row| Self::row_to_pipe_line(row)));        out    }    fn row_to_pipe_line(row: &[TableCell]) -> HyperlinkLine {        let mut out = HyperlinkLine::new(Line::default());        out.push_span("|".into(), /*destination*/ None);        for cell in row {            out.push_span(" ".into(), /*destination*/ None);            for (index, line) in cell.lines.iter().enumerate() {                if index > 0 {                    out.push_span(" ".into(), /*destination*/ None);                }                let text = line                    .line                    .spans                    .iter()                    .map(|span| span.content.as_ref())                    .collect::<String>();                let mut column = 0usize;                let mut current_destination = None;                let mut current_text = String::new();                let flush = |out: &mut HyperlinkLine,                             current_text: &mut String,                             destination: Option<&str>| {                    if !current_text.is_empty() {                        out.push_span(Span::raw(std::mem::take(current_text)), destination);                    }                };                for ch in text.chars() {                    let destination = line                        .hyperlinks                        .iter()                        .find(|link| link.columns.contains(&column))                        .map(|link| link.destination.as_str());                    if destination != current_destination {                        flush(&mut out, &mut current_text, current_destination);                        current_destination = destination;                    }                    if ch == '|' {                        current_text.push_str("\\|");                    } else {                        current_text.push(ch);                    }                    column += UnicodeWidthChar::width(ch).unwrap_or(/*default*/ 0);                }                flush(&mut out, &mut current_text, current_destination);            }            out.push_span(" |".into(), /*destination*/ None);        }        out    }    fn alignments_to_pipe_delimiter(alignments: &[Alignment]) -> String {        let mut out = String::new();        out.push('|');        for alignment in alignments {            let segment = match alignment {                Alignment::Left => ":---",                Alignment::Center => ":---:",                Alignment::Right => "---:",                Alignment::None => "---",            };            out.push_str(segment);            out.push('|');        }        out    }    /// Wrap a single table cell's content to `width`, preserving rich inline    /// styling (bold, code, links) across wrapped lines.    ///    /// Each logical line within the cell (separated by hard breaks) is wrapped    /// independently.  Empty cells produce a single blank line so the row grid    /// stays aligned.    fn wrap_cell(&self, cell: &TableCell, width: usize) -> Vec<HyperlinkLine> {        if cell.lines.is_empty() {            return vec![HyperlinkLine::new(Line::default())];        }        let mut wrapped = Vec::new();        for source_line in &cell.lines {            let rendered =                word_wrap_line(&source_line.line, RtOptions::new(width.max(/*other*/ 1)))                    .into_iter()                    .map(|line| line_to_static(&line))                    .collect::<Vec<_>>();            if rendered.is_empty() {                wrapped.push(HyperlinkLine::new(Line::default()));            } else {                wrapped.extend(remap_wrapped_line(source_line, rendered));            };        }        if wrapped.is_empty() {            wrapped.push(HyperlinkLine::new(Line::default()));        }        wrapped    }    /// Detect rows that are artifacts of pulldown-cmark's lenient table parsing.    ///    /// pulldown-cmark accepts body rows without leading pipes, which can absorb a    /// trailing paragraph as a single-cell row in a multi-column table. These    /// "spillover" rows are extracted and rendered as plain text after the table    /// grid so they don't appear as malformed table content.    ///    /// Heuristic: a row is spillover if its only non-empty cell is the first one    /// AND (a single-cell row lacked table pipe syntax, the content looks like    /// HTML, it's a label line followed by HTML content, or a trailing    /// HTML-intro label line).    fn is_spillover_row(row: &TableBodyRow, next_row: Option<&TableBodyRow>) -> bool {        let Some(first_text) = Self::first_non_empty_only_text(&row.cells) else {            return false;        };        if row.cells.len() == 1 && !row.has_table_pipe_syntax {            return true;        }        if Self::looks_like_html_content(&first_text) {            return true;        }        // Keep common intro + html-block spillover together:        // "HTML block:" followed by "<div ...>".        if first_text.trim_end().ends_with(':') {            if next_row                .and_then(|row| Self::first_non_empty_only_text(&row.cells))                .is_some_and(|text| Self::looks_like_html_content(&text))            {                return true;            }            // pulldown can end the table before the corresponding HTML block line.            // In that case, treat trailing HTML-intro labels (e.g., "HTML block:")            // as spillover while keeping explicit sparse labels in real tables.            if next_row.is_none() && Self::looks_like_html_label_line(&first_text) {                return true;            }        }        false    }    fn first_non_empty_only_text(row: &[TableCell]) -> Option<String> {        let first = row.first()?.plain_text();        if first.trim().is_empty() {            return None;        }        let rest_empty = row[1..]            .iter()            .all(|cell| cell.plain_text().trim().is_empty());        rest_empty.then_some(first)    }    fn looks_like_html_content(text: &str) -> bool {        let bytes = text.as_bytes();        for (idx, &byte) in bytes.iter().enumerate() {            if byte != b'<' {                continue;            }            let mut tag_start = idx + 1;            if tag_start < bytes.len() && (bytes[tag_start] == b'/' || bytes[tag_start] == b'!') {                tag_start += 1;            }            if bytes.get(tag_start).is_some_and(u8::is_ascii_alphabetic)                && bytes                    .get(tag_start + 1..)                    .is_some_and(|suffix| suffix.contains(&b'>'))            {                return true;            }        }        false    }    fn looks_like_html_label_line(text: &str) -> bool {        let trimmed = text.trim();        if !trimmed.ends_with(':') {            return false;        }        let prefix = trimmed.trim_end_matches(':').trim();        prefix            .split_whitespace()            .any(|word| word.eq_ignore_ascii_case("html"))    }    // Width-measurement helpers inlined — called per-cell during table column    // width computation, which runs on every re-render.    #[inline]    fn spans_display_width(spans: &[Span<'_>]) -> usize {        spans.iter().map(|span| span.content.width()).sum()    }    #[inline]    fn line_display_width(line: &Line<'_>) -> usize {        Self::spans_display_width(&line.spans)    }    #[inline]    fn cell_display_width(cell: &TableCell) -> usize {        cell.lines            .iter()            .map(|line| Self::line_display_width(&line.line))            .max()            .unwrap_or(0)    }    #[inline]    fn longest_token_width(text: &str) -> usize {        text.split_whitespace().map(str::width).max().unwrap_or(0)    }    fn push_inline_style(&mut self, style: Style) {        let current = self.inline_styles.last().copied().unwrap_or_default();        let merged = current.patch(style);        self.inline_styles.push(merged);    }    fn pop_inline_style(&mut self) {        self.inline_styles.pop();    }    fn push_link(&mut self, dest_url: String) {        let show_destination = should_render_link_destination(&dest_url);        self.link = Some(LinkState {            show_destination,            local_target_display: if is_local_path_like_link(&dest_url) {                render_local_link_target(&dest_url, self.cwd.as_deref())            } else {                None            },            destination: dest_url,        });    }    fn pop_link(&mut self) {        if let Some(link) = self.link.take() {            if link.show_destination {                // Link destinations are rendered as " (url)" suffixes. When parsing table cells,                // append the suffix into the active cell buffer rather than the outer paragraph                // line to avoid detached url lines.                if self.in_table_cell() {                    self.push_span_to_table_cell(" (".into());                    let mut destination = HyperlinkLine::new(Line::default());                    destination.push_span(                        Span::styled(link.destination.clone(), self.styles.link),                        web_destination(&link.destination).as_deref(),                    );                    if let Some(table_state) = self.table_state.as_mut()                        && let Some(cell) = table_state.current_cell.as_mut()                    {                        cell.push_annotated(destination);                    }                    self.push_span_to_table_cell(")".into());                } else {                    self.push_span(" (".into());                    let mut destination = HyperlinkLine::new(Line::default());                    destination.push_span(                        Span::styled(link.destination.clone(), self.styles.link),                        web_destination(&link.destination).as_deref(),                    );                    self.push_annotated(destination);                    self.push_span(")".into());                }            } else if let Some(local_target_display) = link.local_target_display {                // Local file links are rendered as code-like path text so the transcript shows the                // resolved target instead of arbitrary caller-provided label text.                let style = self                    .inline_styles                    .last()                    .copied()                    .unwrap_or_default()                    .patch(self.styles.code);                let span = Span::styled(local_target_display, style);                if self.in_table_cell() {                    self.push_span_to_table_cell(span);                } else {                    if self.pending_marker_line {                        self.push_line(Line::default());                    }                    self.push_span(span);                    self.line_ends_with_local_link_target = true;                }            }        }    }    fn suppressing_local_link_label(&self) -> bool {        self.link            .as_ref()            .and_then(|link| link.local_target_display.as_ref())            .is_some()    }    fn flush_current_line(&mut self) {        if let Some(mut line) = self.current_line_content.take() {            let style = self.current_line_style;            // NB we don't wrap code in code blocks, in order to preserve whitespace for copy/paste.            if !self.current_line_in_code_block                && let Some(width) = self.wrap_width            {                let opts = RtOptions::new(width)                    .initial_indent(self.current_initial_indent.clone().into())                    .subsequent_indent(self.current_subsequent_indent.clone().into());                let wrapped = adaptive_wrap_line(&line.line, opts)                    .into_iter()                    .map(|wrapped| line_to_static(&wrapped))                    .collect();                for wrapped in remap_wrapped_line(&line, wrapped) {                    self.push_output_line(wrapped.style(style));                }            } else {                let mut spans = self.current_initial_indent.clone();                let shift = spans.iter().map(|span| span.content.width()).sum::<usize>();                spans.append(&mut line.line.spans);                for hyperlink in &mut line.hyperlinks {                    hyperlink.columns =                        hyperlink.columns.start + shift..hyperlink.columns.end + shift;                }                line.line = Line::from_iter(spans);                self.push_output_line(line.style(style));            }            self.current_initial_indent.clear();            self.current_subsequent_indent.clear();            self.current_line_in_code_block = false;            self.line_ends_with_local_link_target = false;        }    }    /// Push a line that has already been laid out at the correct width, skipping    /// word wrapping.    ///    /// Table lines are pre-formatted with exact column widths and separators.    /// Passing them through `word_wrap_line` would break the layout at    /// arbitrary positions. This method prepends the indent/blockquote prefix    /// and pushes directly to `self.text`.    fn is_blockquote_active(&self) -> bool {        self.indent_stack            .iter()            .any(|ctx| ctx.prefix.iter().any(|p| p.content.contains('>')))    }    fn push_prewrapped_line(&mut self, mut line: HyperlinkLine, pending_marker_line: bool) {        self.flush_current_line();        let blockquote_active = self.is_blockquote_active();        let style = if blockquote_active {            self.styles.blockquote.patch(line.line.style)        } else {            line.line.style        };        let mut spans = self.prefix_spans(pending_marker_line);        let shift = spans.iter().map(|span| span.content.width()).sum::<usize>();        spans.append(&mut line.line.spans);        for hyperlink in &mut line.hyperlinks {            hyperlink.columns = hyperlink.columns.start + shift..hyperlink.columns.end + shift;        }        line.line = Line::from(spans);        self.push_output_line(line.style(style));    }    fn push_line(&mut self, line: Line<'static>) {        self.flush_current_line();        let blockquote_active = self.is_blockquote_active();        let style = if blockquote_active {            self.styles.blockquote        } else {            line.style        };        let was_pending = self.pending_marker_line;        self.current_initial_indent = self.prefix_spans(was_pending);        self.current_subsequent_indent = self.prefix_spans(/*pending_marker_line*/ false);        self.current_line_style = style;        self.current_line_content = Some(HyperlinkLine::new(line));        self.current_line_in_code_block = self.in_code_block;        self.line_ends_with_local_link_target = false;        self.pending_marker_line = false;    }    fn push_hyperlink_line(&mut self, line: HyperlinkLine) {        let hyperlinks = line.hyperlinks;        self.push_line(line.line);        if let Some(current) = self.current_line_content.as_mut() {            current.hyperlinks = hyperlinks;        }    }    fn push_span(&mut self, span: Span<'static>) {        if let Some(line) = self.current_line_content.as_mut() {            line.line.push_span(span);        } else {            self.push_line(Line::from(vec![span]));        }    }    fn push_annotated(&mut self, mut appended: HyperlinkLine) {        if self.current_line_content.is_none() {            self.push_line(Line::default());        }        if let Some(line) = self.current_line_content.as_mut() {            let shift = line.width();            line.line.spans.append(&mut appended.line.spans);            line.hyperlinks                .extend(appended.hyperlinks.into_iter().map(|mut link| {                    link.columns = link.columns.start + shift..link.columns.end + shift;                    link                }));        }    }    fn push_text_spans(&mut self, text: &str, style: Style) {        let span = Span::styled(text.to_string(), style);        let destination = self            .link            .as_ref()            .and_then(|link| web_destination(&link.destination));        let annotated = if let Some(destination) = destination {            let mut annotated = HyperlinkLine::new(Line::default());            annotated.push_span(span, Some(&destination));            annotated        } else if self.link.is_some() || self.in_code_block {            HyperlinkLine::new(Line::from(span))        } else {            annotate_web_urls_in_line(Line::from(span))        };        self.push_annotated(annotated);    }    fn push_blank_line(&mut self) {        self.flush_current_line();        if self.indent_stack.iter().all(|ctx| ctx.is_list) {            self.push_output_line(HyperlinkLine::new(Line::default()));        } else {            self.push_line(Line::default());            self.flush_current_line();        }    }    fn push_output_line(&mut self, line: HyperlinkLine) {        self.text.push(line);    }    fn prefix_spans(&self, pending_marker_line: bool) -> Vec<Span<'static>> {        let mut prefix: Vec<Span<'static>> = Vec::new();        let last_marker_index = if pending_marker_line {            self.indent_stack                .iter()                .enumerate()                .rev()                .find_map(|(i, ctx)| if ctx.marker.is_some() { Some(i) } else { None })        } else {            None        };        let last_list_index = self.indent_stack.iter().rposition(|ctx| ctx.is_list);        for (i, ctx) in self.indent_stack.iter().enumerate() {            if pending_marker_line {                if Some(i) == last_marker_index                    && let Some(marker) = &ctx.marker                {                    prefix.extend(marker.iter().cloned());                    continue;                }                if ctx.is_list && last_marker_index.is_some_and(|idx| idx > i) {                    continue;                }            } else if ctx.is_list && Some(i) != last_list_index {                continue;            }            prefix.extend(ctx.prefix.iter().cloned());        }        prefix    }}fn is_local_path_like_link(dest_url: &str) -> bool {    dest_url.starts_with("file://")        || dest_url.starts_with('/')        || dest_url.starts_with("~/")        || dest_url.starts_with("./")        || dest_url.starts_with("../")        || dest_url.starts_with("\\\\")        || matches!(            dest_url.as_bytes(),            [drive, b':', separator, ..]                if drive.is_ascii_alphabetic() && matches!(separator, b'/' | b'\\')        )}/// Parse a local link target into normalized path text plus an optional location suffix.////// This accepts the path shapes Codex emits today: `file://` URLs, absolute and relative paths,/// `~/...`, Windows paths, and `#L..C..` or `:line:col` suffixes.fn render_local_link_target(dest_url: &str, cwd: Option<&Path>) -> Option<String> {    let (path_text, location_suffix) = parse_local_link_target(dest_url)?;    let mut rendered = display_local_link_path(&path_text, cwd);    if let Some(location_suffix) = location_suffix {        rendered.push_str(&location_suffix);    }    Some(rendered)}/// Split a local-link destination into `(normalized_path_text, location_suffix)`.////// The returned path text never includes a trailing `#L..` or `:line[:col]` suffix. Path/// normalization expands `~/...` when possible and rewrites path separators into display-stable/// forward slashes. The suffix, when present, is returned separately in normalized markdown form.////// Returns `None` only when the destination looks like a `file://` URL but cannot be parsed into a/// local path. Plain path-like inputs always return `Some(...)` even if they are relative.fn parse_local_link_target(dest_url: &str) -> Option<(String, Option<String>)> {    if dest_url.starts_with("file://") {        let url = Url::parse(dest_url).ok()?;        let path_text = file_url_to_local_path_text(&url)?;        let location_suffix = url            .fragment()            .and_then(normalize_hash_location_suffix_fragment);        return Some((path_text, location_suffix));    }    let mut path_text = dest_url;    let mut location_suffix = None;    // Prefer `#L..` style fragments when both forms are present so URLs like `path#L10` do not    // get misparsed as a plain path ending in `:10`.    if let Some((candidate_path, fragment)) = dest_url.rsplit_once('#')        && let Some(normalized) = normalize_hash_location_suffix_fragment(fragment)    {        path_text = candidate_path;        location_suffix = Some(normalized);    }    if location_suffix.is_none()        && let Some(suffix) = extract_colon_location_suffix(path_text)    {        let path_len = path_text.len().saturating_sub(suffix.len());        path_text = &path_text[..path_len];        location_suffix = Some(suffix);    }    let decoded_path_text =        urlencoding::decode(path_text).unwrap_or(std::borrow::Cow::Borrowed(path_text));    Some((expand_local_link_path(&decoded_path_text), location_suffix))}/// Normalize a hash fragment like `L12` or `L12C3-L14C9` into the display suffix we render.////// Returns `None` for fragments that are not location references. This deliberately ignores other/// `#...` fragments so non-location hashes stay part of the path text.fn normalize_hash_location_suffix_fragment(fragment: &str) -> Option<String> {    HASH_LOCATION_SUFFIX_RE        .is_match(fragment)        .then(|| format!("#{fragment}"))        .and_then(|suffix| normalize_markdown_hash_location_suffix(&suffix))}/// Extract a trailing `:line`, `:line:col`, or range suffix from a plain path-like string.////// The suffix must occur at the end of the input; embedded colons elsewhere in the path are left/// alone. This is what keeps Windows drive letters like `C:/...` from being misread as locations.fn extract_colon_location_suffix(path_text: &str) -> Option<String> {    COLON_LOCATION_SUFFIX_RE        .find(path_text)        .filter(|matched| matched.end() == path_text.len())        .map(|matched| matched.as_str().to_string())}/// Expand home-relative paths and normalize separators for display.////// If `~/...` cannot be expanded because the home directory is unavailable, the original text still/// goes through separator normalization and is returned as-is otherwise.fn expand_local_link_path(path_text: &str) -> String {    // Expand `~/...` eagerly so home-relative links can participate in the same normalization and    // cwd-relative shortening path as absolute links.    if let Some(rest) = path_text.strip_prefix("~/")        && let Some(home) = home_dir()    {        return normalize_local_link_path_text(&home.join(rest).to_string_lossy());    }    normalize_local_link_path_text(path_text)}/// Convert a `file://` URL into the normalized local-path text used for transcript rendering.////// This prefers `Url::to_file_path()` for standard file URLs. When that rejects Windows-oriented/// encodings, we reconstruct a display path from the host/path parts so UNC paths and drive-letter/// URLs still render sensibly.fn file_url_to_local_path_text(url: &Url) -> Option<String> {    if let Ok(path) = url.to_file_path() {        return Some(normalize_local_link_path_text(&path.to_string_lossy()));    }    // Fall back to string reconstruction for cases `to_file_path()` rejects, especially UNC-style    // hosts and Windows drive paths encoded in URL form.    let mut path_text = url.path().to_string();    if let Some(host) = url.host_str()        && !host.is_empty()        && host != "localhost"    {        path_text = format!("//{host}{path_text}");    } else if matches!(        path_text.as_bytes(),        [b'/', drive, b':', b'/', ..] if drive.is_ascii_alphabetic()    ) {        path_text.remove(0);    }    Some(normalize_local_link_path_text(&path_text))}/// Normalize local-path text into the transcript display form.////// Display normalization is intentionally lexical: it does not touch the filesystem, resolve/// symlinks, or collapse `.` / `..`. It only converts separators to forward slashes and rewrites/// UNC-style `\\\\server\\share` inputs into `//server/share` so later prefix checks operate on a/// stable representation.fn normalize_local_link_path_text(path_text: &str) -> String {    // Render all local link paths with forward slashes so display and prefix stripping are stable    // across mixed Windows and Unix-style inputs.    if let Some(rest) = path_text.strip_prefix("\\\\") {        format!("//{}", rest.replace('\\', "/").trim_start_matches('/'))    } else {        path_text.replace('\\', "/")    }}fn is_absolute_local_link_path(path_text: &str) -> bool {    path_text.starts_with('/')        || path_text.starts_with("//")        || matches!(            path_text.as_bytes(),            [drive, b':', b'/', ..] if drive.is_ascii_alphabetic()        )}/// Remove trailing separators from a local path without destroying root semantics.////// Roots like `/`, `//`, and `C:/` stay intact so callers can still distinguish "the root itself"/// from "a path under the root".fn trim_trailing_local_path_separator(path_text: &str) -> &str {    if path_text == "/" || path_text == "//" {        return path_text;    }    if matches!(path_text.as_bytes(), [drive, b':', b'/'] if drive.is_ascii_alphabetic()) {        return path_text;    }    path_text.trim_end_matches('/')}/// Strip `cwd_text` from the start of `path_text` when `path_text` is strictly underneath it.////// Returns the relative remainder without a leading slash. If the path equals the cwd exactly, this/// returns `None` so callers can keep rendering the full path instead of collapsing it to an empty/// string.fn strip_local_path_prefix<'a>(path_text: &'a str, cwd_text: &str) -> Option<&'a str> {    let path_text = trim_trailing_local_path_separator(path_text);    let cwd_text = trim_trailing_local_path_separator(cwd_text);    if path_text == cwd_text {        return None;    }    // Treat filesystem roots specially so `/tmp/x` under `/` becomes `tmp/x` instead of being    // left unchanged by the generic prefix-stripping branch.    if cwd_text == "/" || cwd_text == "//" {        return path_text.strip_prefix('/');    }    path_text        .strip_prefix(cwd_text)        .and_then(|rest| rest.strip_prefix('/'))}/// Choose the visible path text for a local link after normalization.////// Relative paths stay relative. Absolute paths are shortened against `cwd` only when they are/// lexically underneath it; otherwise the absolute path is preserved. This is display logic only,/// not filesystem canonicalization.fn display_local_link_path(path_text: &str, cwd: Option<&Path>) -> String {    let path_text = normalize_local_link_path_text(path_text);    if !is_absolute_local_link_path(&path_text) {        return path_text;    }    if let Some(cwd) = cwd {        // Only shorten absolute paths that are under the provided session cwd; otherwise preserve        // the original absolute target for clarity.        let cwd_text = normalize_local_link_path_text(&cwd.to_string_lossy());        if let Some(stripped) = strip_local_path_prefix(&path_text, &cwd_text) {            return stripped.to_string();        }    }    path_text}#[cfg(test)]mod markdown_render_tests {    include!("markdown_render_tests.rs");}#[cfg(test)]mod tests {    use super::*;    use pretty_assertions::assert_eq;    use ratatui::text::Text;    fn lines_to_strings(text: &Text<'_>) -> Vec<String> {        text.lines            .iter()            .map(|l| {                l.spans                    .iter()                    .map(|s| s.content.clone())                    .collect::<String>()            })            .collect()    }    #[test]    fn wraps_plain_text_when_width_provided() {        let markdown = "This is a simple sentence that should wrap.";        let rendered = render_markdown_text_with_width(markdown, Some(16));        let lines = lines_to_strings(&rendered);        assert_eq!(            lines,            vec![                "This is a simple".to_string(),                "sentence that".to_string(),                "should wrap.".to_string(),            ]        );    }    #[test]    fn wraps_list_items_preserving_indent() {        let markdown = "- first second third fourth";        let rendered = render_markdown_text_with_width(markdown, Some(14));        let lines = lines_to_strings(&rendered);        assert_eq!(            lines,            vec!["- first second".to_string(), "  third fourth".to_string(),]        );    }    #[test]    fn wraps_nested_lists() {        let markdown =            "- outer item with several words to wrap\n  - inner item that also needs wrapping";        let rendered = render_markdown_text_with_width(markdown, Some(20));        let lines = lines_to_strings(&rendered);        assert_eq!(            lines,            vec![                "- outer item with".to_string(),                "  several words to".to_string(),                "  wrap".to_string(),                "    - inner item".to_string(),                "      that also".to_string(),                "      needs wrapping".to_string(),            ]        );    }    #[test]    fn wraps_ordered_lists() {        let markdown = "1. ordered item contains many words for wrapping";        let rendered = render_markdown_text_with_width(markdown, Some(18));        let lines = lines_to_strings(&rendered);        assert_eq!(            lines,            vec![                "1. ordered item".to_string(),                "   contains many".to_string(),                "   words for".to_string(),                "   wrapping".to_string(),            ]        );    }    #[test]    fn wraps_blockquotes() {        let markdown = "> block quote with content that should wrap nicely";        let rendered = render_markdown_text_with_width(markdown, Some(22));        let lines = lines_to_strings(&rendered);        assert_eq!(            lines,            vec![                "> block quote with".to_string(),                "> content that should".to_string(),                "> wrap nicely".to_string(),            ]        );    }    #[test]    fn wraps_blockquotes_inside_lists() {        let markdown = "- list item\n  > block quote inside list that wraps";        let rendered = render_markdown_text_with_width(markdown, Some(24));        let lines = lines_to_strings(&rendered);        assert_eq!(            lines,            vec![                "- list item".to_string(),                "  > block quote inside".to_string(),                "  > list that wraps".to_string(),            ]        );    }    #[test]    fn wraps_list_items_containing_blockquotes() {        let markdown = "1. item with quote\n   > quoted text that should wrap";        let rendered = render_markdown_text_with_width(markdown, Some(24));        let lines = lines_to_strings(&rendered);        assert_eq!(            lines,            vec![                "1. item with quote".to_string(),                "   > quoted text that".to_string(),                "   > should wrap".to_string(),            ]        );    }    #[test]    fn does_not_wrap_code_blocks() {        let markdown = "````\nfn main() { println!(\"hi from a long line\"); }\n````";        let rendered = render_markdown_text_with_width(markdown, Some(10));        let lines = lines_to_strings(&rendered);        assert_eq!(            lines,            vec!["fn main() { println!(\"hi from a long line\"); }".to_string(),]        );    }    #[test]    fn does_not_split_long_url_like_token_without_scheme() {        let url_like =            "example.test/api/v1/projects/alpha-team/releases/2026-02-17/builds/1234567890";        let rendered = render_markdown_text_with_width(url_like, Some(24));        let lines = lines_to_strings(&rendered);        assert_eq!(            lines.iter().filter(|line| line.contains(url_like)).count(),            1,            "expected full URL-like token in one rendered line, got: {lines:?}"        );    }    #[test]    fn fenced_code_info_string_with_metadata_highlights() {        // CommonMark info strings like "rust,no_run" or "rust title=demo"        // contain metadata after the language token.  The language must be        // extracted (first word / comma-separated token) so highlighting works.        for info in &["rust,no_run", "rust no_run", "rust title=\"demo\""] {            let markdown = format!("```{info}\nfn main() {{}}\n```\n");            let rendered = render_markdown_text(&markdown);            let has_rgb = rendered.lines.iter().any(|line| {                line.spans                    .iter()                    .any(|s| matches!(s.style.fg, Some(ratatui::style::Color::Rgb(..))))            });            assert!(                has_rgb,                "info string \"{info}\" should still produce syntax highlighting"            );        }    }    #[test]    fn crlf_code_block_no_extra_blank_lines() {        // pulldown-cmark can split CRLF code blocks into multiple Text events.        // The buffer must concatenate them verbatim — no inserted separators.        let markdown = "```rust\r\nfn main() {}\r\n    line2\r\n```\r\n";        let rendered = render_markdown_text(markdown);        let lines = lines_to_strings(&rendered);        // Should be exactly two code lines; no spurious blank line between them.        assert_eq!(            lines,            vec!["fn main() {}".to_string(), "    line2".to_string()],            "CRLF code block should not produce extra blank lines: {lines:?}"        );    }    #[test]    fn wrap_cell_preserves_hard_break_lines() {        let mut cell = TableCell::default();        cell.push_span("first line".into());        cell.hard_break();        cell.push_span("second line".into());        let writer = W::new("", std::iter::empty(), Some(80), /*cwd*/ None);        let wrapped = writer.wrap_cell(&cell, /*width*/ 40);        let rendered = wrapped            .iter()            .map(|line| {                line.line                    .spans                    .iter()                    .map(|span| span.content.clone())                    .collect::<String>()            })            .collect::<Vec<_>>();        assert_eq!(            rendered,            vec!["first line".to_string(), "second line".to_string()]        );    }    // ---------------------------------------------------------------    // Type alias for calling private associated functions on Writer.    // ---------------------------------------------------------------    type W<'a> = Writer<'a, std::iter::Empty<(Event<'a>, Range<usize>)>>;    /// Build a single-line `TableCell` from plain text.    fn make_cell(text: &str) -> TableCell {        let mut cell = TableCell::default();        cell.push_span(Span::raw(text.to_string()));        cell    }    fn make_body_row(cells: Vec<TableCell>, has_table_pipe_syntax: bool) -> TableBodyRow {        TableBodyRow {            cells,            has_table_pipe_syntax,        }    }    // ===== Column-metrics unit tests =====    #[test]    fn column_classification_narrative_by_word_count() {        // Col 0: short tokens (1-2 words each) -> Compact        // Col 1: prose (≥4 words per cell) → Narrative        let header = vec![make_cell("ID"), make_cell("Description")];        let rows = vec![            vec![make_cell("1"), make_cell("a long description of the item")],            vec![make_cell("2"), make_cell("another verbose body cell here")],        ];        let metrics = W::collect_table_column_metrics(&header, &rows, /*column_count*/ 2);        assert_eq!(metrics[0].kind, TableColumnKind::Compact);        assert_eq!(metrics[1].kind, TableColumnKind::Narrative);    }    #[test]    fn column_classification_token_heavy_by_url_like_tokens() {        let header = vec![make_cell("URL")];        let rows = vec![            vec![make_cell("https://example.com/very/long/path")],            vec![make_cell("https://another.example.org/deep")],        ];        let metrics = W::collect_table_column_metrics(&header, &rows, /*column_count*/ 1);        assert_eq!(metrics[0].kind, TableColumnKind::TokenHeavy);    }    #[test]    fn column_classification_token_heavy_for_local_path_lists() {        let header = vec![make_cell("Files")];        let rows = vec![            vec![make_cell(                "codex-rs/core/src/next_prompt_suggestion.rs:1, codex-rs/core/src/next_prompt_suggestion_tests.rs:1",            )],            vec![make_cell(                "codex-rs/core/src/context/next_prompt_suggestion.rs:1, codex-rs/core/src/context/contextual_user_message_tests.rs:1",            )],        ];        let metrics = W::collect_table_column_metrics(&header, &rows, /*column_count*/ 1);        assert_eq!(metrics[0].kind, TableColumnKind::TokenHeavy);    }    #[test]    fn column_classification_compact_all_short() {        // Both columns short tokens -> both Compact        let header = vec![make_cell("Status"), make_cell("Count")];        let rows = vec![            vec![make_cell("ok"), make_cell("42")],            vec![make_cell("err"), make_cell("7")],        ];        let metrics = W::collect_table_column_metrics(&header, &rows, /*column_count*/ 2);        assert_eq!(metrics[0].kind, TableColumnKind::Compact);        assert_eq!(metrics[1].kind, TableColumnKind::Compact);    }    #[test]    fn preferred_floor_narrative_retains_readable_width() {        let m = TableColumnMetrics {            max_width: 40,            header_token_width: 15,            body_token_width: 8,            kind: TableColumnKind::Narrative,        };        assert_eq!(W::preferred_column_floor(&m, /*min_column_width*/ 3), 16);        let m2 = TableColumnMetrics {            max_width: 12,            header_token_width: 6,            body_token_width: 8,            kind: TableColumnKind::Narrative,        };        assert_eq!(W::preferred_column_floor(&m2, /*min_column_width*/ 3), 12);    }    #[test]    fn preferred_floor_token_heavy_retains_readable_width() {        let m = TableColumnMetrics {            max_width: 80,            header_token_width: 5,            body_token_width: 60,            kind: TableColumnKind::TokenHeavy,        };        assert_eq!(W::preferred_column_floor(&m, /*min_column_width*/ 3), 16);    }    #[test]    fn preferred_floor_compact_uses_body_token() {        // Compact: max(header_token_width, body_token_width.min(16))        let m = TableColumnMetrics {            max_width: 30,            header_token_width: 5,            body_token_width: 12,            kind: TableColumnKind::Compact,        };        // max(5, min(12, 16)) = max(5, 12) = 12        assert_eq!(W::preferred_column_floor(&m, /*min_column_width*/ 3), 12);        // Body token exceeds 16 cap → capped at 16, then max with header        let m2 = TableColumnMetrics {            max_width: 30,            header_token_width: 5,            body_token_width: 20,            kind: TableColumnKind::Compact,        };        // max(5, min(20, 16)) = max(5, 16) = 16        assert_eq!(W::preferred_column_floor(&m2, /*min_column_width*/ 3), 16);    }    #[test]    fn next_column_to_shrink_prefers_token_heavy_then_narrative() {        let widths = [20usize, 20, 20];        let floors = [8usize, 8, 8];        let metrics = [            TableColumnMetrics {                max_width: 30,                header_token_width: 8,                body_token_width: 6,                kind: TableColumnKind::Narrative,            },            TableColumnMetrics {                max_width: 30,                header_token_width: 8,                body_token_width: 28,                kind: TableColumnKind::TokenHeavy,            },            TableColumnMetrics {                max_width: 30,                header_token_width: 8,                body_token_width: 6,                kind: TableColumnKind::Compact,            },        ];        let idx = W::next_column_to_shrink(&widths, &floors, &metrics);        assert_eq!(idx, Some(1), "token-heavy column should shrink first");        let widths = [20usize, 8, 20];        let idx = W::next_column_to_shrink(&widths, &floors, &metrics);        assert_eq!(            idx,            Some(0),            "narrative column should shrink before compact"        );    }    // ===== Spillover-detection unit tests =====    #[test]    fn spillover_detects_single_cell_row() {        let row = make_body_row(            vec![make_cell("some trailing text")],            /*has_table_pipe_syntax*/ false,        );        assert!(W::is_spillover_row(&row, /*next_row*/ None));    }    #[test]    fn spillover_keeps_single_cell_row_with_table_pipe_syntax() {        let row = make_body_row(            vec![make_cell("some sparse value")],            /*has_table_pipe_syntax*/ true,        );        assert!(!W::is_spillover_row(&row, /*next_row*/ None));    }    #[test]    fn spillover_detects_html_content() {        // 3-cell row where only cell 0 has HTML content        let row = make_body_row(            vec![                make_cell("<div>content</div>"),                make_cell(""),                make_cell(""),            ],            /*has_table_pipe_syntax*/ false,        );        assert!(W::is_spillover_row(&row, /*next_row*/ None));    }    #[test]    fn spillover_detects_label_followed_by_html() {        // cell 0 = "HTML block:" and next_row cell 0 = "<div>x</div>"        let row = make_body_row(            vec![make_cell("HTML block:"), make_cell(""), make_cell("")],            /*has_table_pipe_syntax*/ false,        );        let next = make_body_row(            vec![make_cell("<div>x</div>"), make_cell(""), make_cell("")],            /*has_table_pipe_syntax*/ false,        );        assert!(W::is_spillover_row(&row, Some(&next)));    }    #[test]    fn spillover_detects_trailing_html_label() {        // "HTML block:" with no next_row → trailing HTML label spillover        let row = make_body_row(            vec![make_cell("HTML block:"), make_cell(""), make_cell("")],            /*has_table_pipe_syntax*/ false,        );        assert!(W::is_spillover_row(&row, /*next_row*/ None));    }    #[test]    fn spillover_keeps_normal_multi_cell_row() {        // 3 cells all non-empty → not spillover        let row = make_body_row(            vec![make_cell("one"), make_cell("two"), make_cell("three")],            /*has_table_pipe_syntax*/ true,        );        assert!(!W::is_spillover_row(&row, /*next_row*/ None));    }    #[test]    fn spillover_keeps_label_when_next_is_not_html() {        // cell 0 = "Status:" and next_row cell 0 = "ok" → not spillover (not HTML)        let row = make_body_row(            vec![make_cell("Status:"), make_cell(""), make_cell("")],            /*has_table_pipe_syntax*/ true,        );        let next = make_body_row(            vec![make_cell("ok"), make_cell(""), make_cell("")],            /*has_table_pipe_syntax*/ true,        );        assert!(!W::is_spillover_row(&row, Some(&next)));    }    #[test]    fn annotates_explicit_web_link_label_and_visible_destination() {        let lines = render_markdown_lines_with_width_and_cwd(            "See [docs](https://example.com/reference).",            /*width*/ Some(80),            /*cwd*/ None,        );        let links = lines            .iter()            .flat_map(|line| line.hyperlinks.iter())            .collect::<Vec<_>>();        assert_eq!(links.len(), 2);        assert!(            links                .iter()                .all(|link| link.destination == "https://example.com/reference")        );    }    #[test]    fn wrapped_table_url_fragments_keep_complete_web_destination() {        let destination = "https://example.com/a/very/long/path/to/a/table/artifact";        let markdown = format!("| Item | URL |\n| --- | --- |\n| report | {destination} |\n");        let lines = render_markdown_lines_with_width_and_cwd(            &markdown,            /*width*/ Some(32),            /*cwd*/ None,        );        let linked_rows = lines            .iter()            .filter(|line| !line.hyperlinks.is_empty())            .collect::<Vec<_>>();        assert!(            linked_rows.len() > 1,            "expected a URL wrapped across table rows"        );        assert!(linked_rows.iter().all(|line| {            line.hyperlinks                .iter()                .all(|link| link.destination == destination)        }));    }    #[test]    fn key_value_table_keeps_web_annotations() {        let destination = "https://example.com/a/very/long/path";        let markdown = format!(            "| c1 | c2 | c3 | c4 | c5 | c6 |\n| --- | --- | --- | --- | --- | --- |\n| {destination} | 2 | 3 | 4 | 5 | 6 |\n"        );        let lines = render_markdown_lines_with_width_and_cwd(            &markdown,            /*width*/ Some(20),            /*cwd*/ None,        );        let destinations = lines            .iter()            .flat_map(|line| line.hyperlinks.iter().map(|link| link.destination.as_str()))            .collect::<Vec<_>>();        assert!(!destinations.is_empty());        assert!(destinations.iter().all(|link| *link == destination));    }    #[test]    fn does_not_annotate_code_or_non_web_markdown_links() {        let markdown = "`https://example.com/inline`\n\n```text\nhttps://example.com/block\n```\n\n[mail](mailto:test@example.com)\n\n[https://example.com/label](mailto:test@example.com)\n\n| Target |\n| --- |\n| [https://example.com/table-label](mailto:test@example.com) |";        let lines = render_markdown_lines_with_width_and_cwd(            markdown,            /*width*/ Some(80),            /*cwd*/ None,        );        assert!(lines.iter().all(|line| line.hyperlinks.is_empty()));    }    #[test]    fn pipe_table_fallback_keeps_web_annotations() {        let destination = "https://example.com/a/long/path";        let target = "https://target.example/path";        let code_url = "https://code.example/not-a-link";        let markdown = format!(            "| URL | Code | Label |\n| --- | --- | --- |\n| {destination} | `{code_url}` | [https://shown.example]({target}) |\n"        );        let lines = render_markdown_lines_with_width_and_cwd(            &markdown,            /*width*/ Some(5),            /*cwd*/ None,        );        let destinations = lines            .iter()            .flat_map(|line| line.hyperlinks.iter().map(|link| link.destination.as_str()))            .collect::<Vec<_>>();        assert!(destinations.contains(&destination));        assert!(destinations.contains(&target));        assert!(!destinations.contains(&code_url));        assert!(!destinations.contains(&"https://shown.example"));    }}