use ratatui::{
style::{Modifier, Style},
text::{Line, Span},
};
use unicode_width::UnicodeWidthStr;
use super::theme::THEME;
use super::highlight::highlight_code_block;
pub(crate) fn parse_inline_md(text: &str, base_style: Style) -> Vec<Span<'static>> {
let mut spans: Vec<Span<'static>> = Vec::new();
let mut chars = text.chars().peekable();
let mut buf = String::new();
let bold_style = base_style.add_modifier(Modifier::BOLD);
let italic_style = base_style.add_modifier(Modifier::ITALIC);
let code_style = Style::default().fg(THEME.load().code_fg).bg(THEME.load().code_bg);
while let Some(ch) = chars.next() {
match ch {
'`' => {
if !buf.is_empty() {
spans.push(Span::styled(buf.clone(), base_style));
buf.clear();
}
let mut code = String::new();
while let Some(&c) = chars.peek() {
if c == '`' { chars.next(); break; }
code.push(c);
chars.next();
}
if !code.is_empty() {
spans.push(Span::styled(format!(" {} ", code), code_style));
}
}
'*' => {
if !buf.is_empty() {
spans.push(Span::styled(buf.clone(), base_style));
buf.clear();
}
let is_bold = chars.peek() == Some(&'*');
if is_bold { chars.next(); }
let delim = if is_bold { "**" } else { "*" };
let mut inner = String::new();
loop {
match chars.next() {
Some('*') if is_bold => {
if chars.peek() == Some(&'*') { chars.next(); break; }
inner.push('*');
}
Some('*') if !is_bold => break,
Some(c) => inner.push(c),
None => { inner = format!("{}{}", delim, inner); break; }
}
}
let style = if is_bold { bold_style } else { italic_style };
spans.push(Span::styled(inner, style));
}
_ => buf.push(ch),
}
}
if !buf.is_empty() {
spans.push(Span::styled(buf, base_style));
}
spans
}
fn strip_inline_md(text: &str) -> String {
let mut result = String::new();
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'`' => {
while let Some(&c) = chars.peek() {
if c == '`' { chars.next(); break; }
result.push(c);
chars.next();
}
}
'*' => {
let is_bold = chars.peek() == Some(&'*');
if is_bold { chars.next(); } let mut found_end = false;
while let Some(c) = chars.next() {
if c == '*' {
if is_bold {
if chars.peek() == Some(&'*') { chars.next(); found_end = true; break; }
result.push(c);
} else {
found_end = true;
break;
}
} else {
result.push(c);
}
}
let _ = found_end;
}
_ => result.push(ch),
}
}
result
}
fn wrap_cell(text: &str, max_width: usize) -> Vec<String> {
if max_width == 0 {
return vec![String::new()];
}
let stripped = strip_inline_md(text);
let display_w = UnicodeWidthStr::width(stripped.as_str());
if display_w <= max_width {
return vec![text.to_string()];
}
let mut lines: Vec<String> = Vec::new();
let mut current = String::new();
let mut current_w: usize = 0;
for word in text.split_whitespace() {
let word_w = UnicodeWidthStr::width(word);
if current.is_empty() {
if word_w <= max_width {
current.push_str(word);
current_w = word_w;
} else {
for ch in word.chars() {
let ch_w = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0);
if current_w + ch_w > max_width && !current.is_empty() {
lines.push(current);
current = String::new();
current_w = 0;
}
current.push(ch);
current_w += ch_w;
}
}
} else if current_w + 1 + word_w <= max_width {
current.push(' ');
current.push_str(word);
current_w += 1 + word_w;
} else {
lines.push(current);
current = String::new();
current_w = 0;
if word_w <= max_width {
current.push_str(word);
current_w = word_w;
} else {
for ch in word.chars() {
let ch_w = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0);
if current_w + ch_w > max_width && !current.is_empty() {
lines.push(current);
current = String::new();
current_w = 0;
}
current.push(ch);
current_w += ch_w;
}
}
}
}
if !current.is_empty() {
lines.push(current);
}
if lines.is_empty() {
lines.push(String::new());
}
lines
}
pub(crate) fn render_table(table_lines: &[String], prefix: &str, width: usize) -> Vec<Line<'static>> {
let mut result: Vec<Line> = Vec::new();
if table_lines.is_empty() {
return result;
}
let mut rows: Vec<Vec<String>> = Vec::new();
let mut has_header = false;
for (i, line) in table_lines.iter().enumerate() {
let stripped = line.trim().trim_matches('|');
let is_separator = stripped.split('|').all(|cell| {
let c = cell.trim();
!c.is_empty() && c.chars().all(|ch| ch == '-' || ch == ':' || ch == ' ')
});
if is_separator {
if i == 1 {
has_header = true;
}
continue;
}
let cells: Vec<String> = stripped
.split('|')
.map(|c| c.trim().to_string())
.collect();
rows.push(cells);
}
if rows.is_empty() {
return result;
}
let num_cols = rows.iter().map(|r| r.len()).max().unwrap_or(0);
for row in &mut rows {
while row.len() < num_cols {
row.push(String::new());
}
}
let mut col_widths: Vec<usize> = vec![3; num_cols];
for row in &rows {
for (j, cell) in row.iter().enumerate() {
if j < num_cols {
let stripped = strip_inline_md(cell);
col_widths[j] = col_widths[j].max(UnicodeWidthStr::width(stripped.as_str()));
}
}
}
let prefix_overhead = UnicodeWidthStr::width(prefix) + 2; let per_col_overhead = 3; let total_table_width = prefix_overhead + col_widths.iter().sum::<usize>() + num_cols * per_col_overhead;
if width > 0 && total_table_width > width {
let available = width.saturating_sub(prefix_overhead + num_cols * per_col_overhead);
if available > 0 && num_cols > 0 {
let total_content: usize = col_widths.iter().sum();
if total_content > available {
let narrow_threshold = 12;
let mut new_widths = col_widths.clone();
let mut locked_total: usize = 0;
let mut shrinkable_total: usize = 0;
let mut shrinkable_indices: Vec<usize> = Vec::new();
for (i, &w) in col_widths.iter().enumerate() {
if w <= narrow_threshold {
locked_total += w;
} else {
shrinkable_indices.push(i);
shrinkable_total += w;
}
}
let budget_for_shrinkable = available.saturating_sub(locked_total);
if shrinkable_indices.is_empty() || budget_for_shrinkable == 0 {
new_widths = col_widths.iter()
.map(|&w| (w * available / total_content).max(3))
.collect();
} else {
for &idx in &shrinkable_indices {
let share = (col_widths[idx] * budget_for_shrinkable / shrinkable_total).max(6);
new_widths[idx] = share;
}
}
let used: usize = new_widths.iter().sum();
if used < available {
let mut remainder = available - used;
let mut indices: Vec<usize> = (0..num_cols).collect();
indices.sort_by(|a, b| col_widths[*b].cmp(&col_widths[*a]));
for &idx in &indices {
if remainder == 0 { break; }
new_widths[idx] += 1;
remainder -= 1;
}
}
col_widths = new_widths;
}
}
}
let border_style = Style::default().fg(THEME.load().table_border_color);
let header_style = Style::default().fg(THEME.load().table_header_color).add_modifier(Modifier::BOLD);
let cell_style = Style::default().fg(THEME.load().table_cell_color);
result.push(Line::from(""));
let body_start = if has_header { 1 } else { 0 };
let body_count = rows.len().saturating_sub(body_start);
for (i, row) in rows.iter().enumerate() {
let style = if has_header && i == 0 { header_style } else { cell_style };
let mut wrapped_cols: Vec<Vec<String>> = Vec::new();
for (j, cell) in row.iter().enumerate() {
let w = col_widths[j];
wrapped_cols.push(wrap_cell(cell, w));
}
let max_lines = wrapped_cols.iter().map(|c| c.len()).max().unwrap_or(1);
for line_idx in 0..max_lines {
let mut spans: Vec<Span> = Vec::new();
spans.push(Span::styled(format!("{} ", prefix), Style::default()));
for (j, col_lines) in wrapped_cols.iter().enumerate() {
let w = col_widths[j];
let cell_text = col_lines.get(line_idx).map(|s| s.as_str()).unwrap_or("");
let stripped_text = strip_inline_md(cell_text);
let display_w = UnicodeWidthStr::width(stripped_text.as_str());
let padding = w.saturating_sub(display_w);
spans.push(Span::styled(" ".to_string(), style));
spans.extend(parse_inline_md(cell_text, style));
spans.push(Span::styled(format!("{} ", " ".repeat(padding)), style));
if j < num_cols - 1 {
spans.push(Span::styled(" ", Style::default()));
}
}
result.push(Line::from(spans));
}
if max_lines > 1 && i >= body_start && i < rows.len() - 1 {
let rule_width: usize = col_widths.iter().sum::<usize>() + num_cols * 3;
let sep = format!("{} {}", prefix, "\u{2508}".repeat(rule_width.min(width.saturating_sub(UnicodeWidthStr::width(prefix) + 2))));
result.push(Line::from(Span::styled(
sep,
Style::default().fg(THEME.load().table_border_color).add_modifier(Modifier::DIM),
)));
}
if has_header && i == 0 {
let rule_width: usize = col_widths.iter().sum::<usize>() + num_cols * 3;
let sep = format!("{} {}", prefix, "\u{2500}".repeat(rule_width.min(width.saturating_sub(UnicodeWidthStr::width(prefix) + 2))));
result.push(Line::from(Span::styled(sep, border_style)));
}
if has_header && i > 0 && body_count > 6 {
let body_idx = i - body_start; if body_idx > 0 && body_idx % 5 == 0 && i < rows.len() - 1 {
let rule_width: usize = col_widths.iter().sum::<usize>() + num_cols * 3;
let sep = format!("{} {}", prefix, "\u{2500}".repeat(rule_width.min(width.saturating_sub(UnicodeWidthStr::width(prefix) + 2))));
result.push(Line::from(Span::styled(
sep,
Style::default().fg(THEME.load().table_border_color).add_modifier(Modifier::DIM),
)));
}
} else if !has_header && body_count > 6 {
if i > 0 && i % 5 == 0 && i < rows.len() - 1 {
let rule_width: usize = col_widths.iter().sum::<usize>() + num_cols * 3;
let sep = format!("{} {}", prefix, "\u{2500}".repeat(rule_width.min(width.saturating_sub(UnicodeWidthStr::width(prefix) + 2))));
result.push(Line::from(Span::styled(
sep,
Style::default().fg(THEME.load().table_border_color).add_modifier(Modifier::DIM),
)));
}
}
}
result.push(Line::from(""));
result
}
pub(crate) fn render_markdown(text: &str, prefix: &str, width: usize) -> Vec<Line<'static>> {
let mut lines: Vec<Line> = Vec::new();
let base_style = Style::default().fg(THEME.load().claude_text);
let mut in_code_block = false;
let mut code_lang = String::new();
let mut code_buf = String::new();
let mut table_buf: Vec<String> = Vec::new();
let all_lines: Vec<&str> = text.lines().collect();
for (line_idx, line) in all_lines.iter().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("```") {
if !table_buf.is_empty() {
lines.extend(render_table(&table_buf, prefix, width));
table_buf.clear();
}
if !in_code_block {
in_code_block = true;
code_lang = trimmed.strip_prefix("```").unwrap_or("").trim().to_string();
code_buf.clear();
} else {
let border_style = Style::default().fg(THEME.load().border);
let lang_style = Style::default().fg(THEME.load().muted).add_modifier(Modifier::DIM);
let block_inner_width = width.saturating_sub(UnicodeWidthStr::width(prefix) + 4); let rule_width = block_inner_width.min(60).max(20);
lines.push(Line::from("")); if !code_lang.is_empty() {
let lang_upper = code_lang.to_uppercase();
let spaced: String = lang_upper.chars()
.enumerate()
.map(|(i, c)| if i > 0 { format!(" {}", c) } else { c.to_string() })
.collect();
lines.push(Line::from(vec![
Span::styled(format!("{} ", prefix), Style::default()),
Span::styled(spaced, lang_style),
]));
}
lines.push(Line::from(Span::styled(
format!("{} {}", prefix, "\u{2500}".repeat(rule_width)),
border_style,
)));
for hl_line in highlight_code_block(&code_buf, &code_lang, prefix) {
lines.push(super::highlight::clamp_line(hl_line, width));
}
lines.push(Line::from(Span::styled(
format!("{} {}", prefix, "\u{2500}".repeat(rule_width)),
border_style,
)));
lines.push(Line::from("")); in_code_block = false;
}
continue;
}
if in_code_block {
code_buf.push_str(line);
code_buf.push('\n');
continue;
}
let is_table_line = trimmed.contains('|') && {
let stripped = trimmed.trim_matches('|').trim();
!stripped.is_empty()
};
if is_table_line {
table_buf.push(trimmed.to_string());
let next_is_table = if line_idx + 1 < all_lines.len() {
let next = all_lines[line_idx + 1].trim();
next.contains('|') && {
let s = next.trim_matches('|').trim();
!s.is_empty()
}
} else {
false
};
if !next_is_table {
lines.extend(render_table(&table_buf, prefix, width));
table_buf.clear();
}
continue;
}
if !table_buf.is_empty() {
lines.extend(render_table(&table_buf, prefix, width));
table_buf.clear();
}
if trimmed.starts_with('#') {
let level = trimmed.chars().take_while(|&c| c == '#').count();
let heading_text = trimmed[level..].trim();
if !lines.is_empty() {
lines.push(Line::from(""));
}
let full = format!("{} {}", prefix, heading_text);
for wline in wrap_text(&full, width) {
lines.push(Line::from(Span::styled(
wline,
Style::default().fg(THEME.load().heading_color).add_modifier(Modifier::BOLD),
)));
}
continue;
}
if trimmed.starts_with('>') {
let quote_text = trimmed.strip_prefix('>').unwrap_or("").trim();
let full = format!("{} \u{2502} {}", prefix, quote_text);
for wline in wrap_text(&full, width) {
lines.push(Line::from(Span::styled(wline, Style::default().fg(THEME.load().quote_color).add_modifier(Modifier::ITALIC))));
}
continue;
}
if trimmed.starts_with("- ") || trimmed.starts_with("* ") {
let item_text = &trimmed[2..];
let bullet_prefix = format!("{} \u{2022} ", prefix);
let cont_prefix = format!("{} ", prefix);
let flat = format!("{}{}", bullet_prefix, item_text);
if flat.chars().count() <= width {
let bullet_span = Span::styled(bullet_prefix, Style::default().fg(THEME.load().list_bullet_color));
let mut item_spans = parse_inline_md(item_text, base_style);
let mut all_spans = vec![bullet_span];
all_spans.append(&mut item_spans);
lines.push(Line::from(all_spans));
} else {
for (li, wline) in wrap_text(&flat, width).into_iter().enumerate() {
if li == 0 {
let inner = if wline.starts_with(&bullet_prefix) {
&wline[bullet_prefix.len()..]
} else {
&wline
};
let bullet_span = Span::styled(bullet_prefix.clone(), Style::default().fg(THEME.load().list_bullet_color));
let mut all_spans = vec![bullet_span];
all_spans.extend(parse_inline_md(inner, base_style));
lines.push(Line::from(all_spans));
} else {
let mut all_spans = vec![Span::styled(cont_prefix.clone(), base_style)];
all_spans.extend(parse_inline_md(wline.trim_start(), base_style));
lines.push(Line::from(all_spans));
}
}
}
continue;
}
if trimmed.len() > 2 {
let num_end = trimmed.find(". ");
if let Some(pos) = num_end {
if pos <= 3 && trimmed[..pos].chars().all(|c| c.is_ascii_digit()) {
let item_text = &trimmed[pos + 2..];
let num_prefix = format!("{} {}. ", prefix, &trimmed[..pos]);
let cont_prefix = format!("{} ", prefix);
let flat = format!("{}{}", num_prefix, item_text);
if flat.chars().count() <= width {
let num_span = Span::styled(num_prefix, Style::default().fg(THEME.load().list_bullet_color));
let mut item_spans = parse_inline_md(item_text, base_style);
let mut all_spans = vec![num_span];
all_spans.append(&mut item_spans);
lines.push(Line::from(all_spans));
} else {
for (li, wline) in wrap_text(&flat, width).into_iter().enumerate() {
if li == 0 {
let inner = if wline.starts_with(&num_prefix) {
&wline[num_prefix.len()..]
} else {
&wline
};
let num_span = Span::styled(num_prefix.clone(), Style::default().fg(THEME.load().list_bullet_color));
let mut all_spans = vec![num_span];
all_spans.extend(parse_inline_md(inner, base_style));
lines.push(Line::from(all_spans));
} else {
let mut all_spans = vec![Span::styled(cont_prefix.clone(), base_style)];
all_spans.extend(parse_inline_md(wline.trim_start(), base_style));
lines.push(Line::from(all_spans));
}
}
}
continue;
}
}
}
if trimmed.is_empty() {
lines.push(Line::from(""));
continue;
}
let full_prefix = format!("{} ", prefix);
let spans = parse_inline_md(line, base_style);
let flat: String = spans.iter().map(|s| s.content.as_ref()).collect();
let full = format!("{}{}", full_prefix, flat);
if full.chars().count() <= width {
let mut line_spans = vec![Span::styled(full_prefix, base_style)];
line_spans.extend(spans);
lines.push(Line::from(line_spans));
} else {
for wline in wrap_text(&full, width) {
let (prefix_part, inner) = if wline.starts_with(&full_prefix) {
(full_prefix.clone(), &wline[full_prefix.len()..])
} else {
let indent_len = wline.chars().take_while(|c| *c == ' ').count();
let indent: String = " ".repeat(indent_len);
let rest = &wline[indent.len()..];
(indent, rest)
};
let parsed = parse_inline_md(inner, base_style);
let mut line_spans = vec![Span::styled(prefix_part, base_style)];
line_spans.extend(parsed);
lines.push(Line::from(line_spans));
}
}
}
lines
}
const TAB_WIDTH: usize = 4;
fn expand_tabs_with_anchor(input: &str) -> (String, Option<usize>) {
let mut out = String::with_capacity(input.len());
let mut anchor: Option<usize> = None;
for ch in input.chars() {
if ch == '\t' {
let col = out.chars().count();
let pad = TAB_WIDTH - (col % TAB_WIDTH);
for _ in 0..pad {
out.push(' ');
}
anchor = Some(out.chars().count());
} else {
out.push(ch);
}
}
(out, anchor)
}
#[allow(unused_assignments)]
pub(crate) fn wrap_text(raw_text: &str, width: usize) -> Vec<String> {
let (text, tab_anchor) = expand_tabs_with_anchor(raw_text);
if width == 0 || text.chars().count() <= width {
return vec![text];
}
let leading_indent = text.chars().take_while(|c| *c == ' ').count();
let cont_indent_len = match tab_anchor {
Some(a) if a + 16 <= width => a,
_ => leading_indent,
};
let indent: String = " ".repeat(cont_indent_len);
let wrap_width = width.saturating_sub(cont_indent_len);
let mut lines = Vec::new();
let mut current = String::new();
let mut is_first_line = true;
for word in text.split_inclusive(' ') {
let wlen = word.chars().count();
let col = current.chars().count();
let effective_width = if is_first_line { width } else { wrap_width };
if col + wlen > effective_width && col > 0 {
lines.push(current.trim_end().to_string());
current = indent.clone();
is_first_line = false;
}
let effective_width = if is_first_line { width } else { wrap_width };
if wlen > effective_width {
let chars: Vec<char> = word.chars().collect();
let chunk_size = effective_width.max(1); for chunk in chars.chunks(chunk_size) {
if !current.is_empty() && current != indent {
lines.push(current.trim_end().to_string());
current = indent.clone();
is_first_line = false;
}
current.push_str(&chunk.iter().collect::<String>());
}
} else {
current.push_str(word);
}
}
if !current.is_empty() {
lines.push(current.trim_end().to_string());
}
if lines.is_empty() {
lines.push(String::new());
}
lines
}
pub(crate) fn format_tokens(n: u64) -> String {
if n >= 1_000_000 { format!("{:.1}M", n as f64 / 1_000_000.0) }
else if n >= 1_000 { format!("{:.1}k", n as f64 / 1_000.0) }
else { format!("{}", n) }
}
#[cfg(test)]
mod tests {
use super::*;
fn rendered_text(lines: &[Line]) -> Vec<String> {
lines.iter().map(|l| {
l.spans.iter().map(|s| s.content.as_ref()).collect::<String>()
}).collect()
}
#[test]
fn table_narrow_cols_preserved_when_shrinking() {
let table_lines = vec![
"| Status | Name | Description |".to_string(),
"|--------|------|-------------|".to_string(),
"| ✅ | Spike | The executor workhorse agent that handles grunt work |".to_string(),
"| ❌ | Chrollo | Deep analyst for recon and complex reasoning tasks |".to_string(),
];
let result = render_table(&table_lines, " ", 60);
let texts = rendered_text(&result);
let has_checkmark = texts.iter().any(|t| t.contains('✅'));
let has_cross = texts.iter().any(|t| t.contains('❌'));
assert!(has_checkmark, "Status ✅ was truncated away");
assert!(has_cross, "Status ❌ was truncated away");
let has_spike = texts.iter().any(|t| t.contains("Spike"));
let has_chrollo = texts.iter().any(|t| t.contains("Chrollo"));
assert!(has_spike, "Name 'Spike' was truncated");
assert!(has_chrollo, "Name 'Chrollo' was truncated");
}
#[test]
fn table_six_columns_dont_all_become_ellipsis() {
let table_lines = vec![
"| # | Test | Status | Quality | Speed | Notes |".to_string(),
"|---|------|--------|---------|-------|-------|".to_string(),
"| 1 | example.com | ✅ Yes | Good | 0.13s | Perfect output |".to_string(),
"| 2 | HN | ✅ Yes | Meh | 0.41s | Table-noisy but works |".to_string(),
];
let result = render_table(&table_lines, " ", 80);
let texts = rendered_text(&result);
let row1 = texts.iter().find(|t| t.contains("example")).unwrap();
assert!(row1.contains("0.13s"), "Speed column was truncated in: {}", row1);
assert!(row1.contains("✅"), "Status was truncated in: {}", row1);
}
#[test]
fn table_fits_width_no_truncation() {
let table_lines = vec![
"| A | B |".to_string(),
"|---|---|".to_string(),
"| x | y |".to_string(),
];
let result = render_table(&table_lines, "", 80);
let texts = rendered_text(&result);
assert!(!texts.iter().any(|t| t.contains('\u{2026}')),
"Small table was unnecessarily truncated");
}
#[test]
fn wrap_cell_fits_returns_single_line() {
let result = wrap_cell("hello world", 20);
assert_eq!(result, vec!["hello world"]);
}
#[test]
fn wrap_cell_breaks_on_word_boundary() {
let result = wrap_cell("hello world foo", 11);
assert_eq!(result, vec!["hello world", "foo"]);
}
#[test]
fn wrap_cell_force_breaks_long_word() {
let result = wrap_cell("abcdefghij", 5);
assert_eq!(result, vec!["abcde", "fghij"]);
}
#[test]
fn table_wraps_instead_of_truncating() {
let table_lines = vec![
"| Name | Description |".to_string(),
"|------|-------------|".to_string(),
"| Spike | The executor workhorse agent that handles all grunt work |".to_string(),
];
let result = render_table(&table_lines, "", 40);
let texts = rendered_text(&result);
let all_text: String = texts.join(" ");
assert!(all_text.contains("grunt work"),
"Wrapped table lost content: {}", all_text);
assert!(!texts.iter().any(|t| t.contains('\u{2026}')),
"Table used truncation instead of wrapping");
}
#[test]
fn table_renders_bold_markdown_in_cells() {
let table_lines = vec![
"| Category | Value |".to_string(),
"|----------|-------|".to_string(),
"| **Era** | 130 million years |".to_string(),
];
let result = render_table(&table_lines, "", 80);
let texts = rendered_text(&result);
let all_text: String = texts.join(" ");
assert!(!all_text.contains("**"), "Bold markers ** still visible in: {}", all_text);
assert!(all_text.contains("Era"), "Bold content 'Era' missing from: {}", all_text);
let era_line = result.iter().find(|l| {
l.spans.iter().any(|s| s.content.contains("Era"))
}).expect("No line contains 'Era'");
let era_span = era_line.spans.iter().find(|s| s.content.contains("Era")).unwrap();
assert!(era_span.style.add_modifier == Modifier::BOLD,
"Era span is not bold: {:?}", era_span.style);
}
#[test]
fn strip_inline_md_removes_bold_markers() {
assert_eq!(strip_inline_md("**hello**"), "hello");
assert_eq!(strip_inline_md("**a** and **b**"), "a and b");
assert_eq!(strip_inline_md("plain text"), "plain text");
assert_eq!(strip_inline_md("*italic*"), "italic");
assert_eq!(strip_inline_md("`code`"), "code");
}
#[test]
fn table_bold_cells_dont_waste_width_on_markers() {
let table_lines = vec![
"| Label | Data |".to_string(),
"|-------|------|".to_string(),
"| **Signature Move** | some data |".to_string(),
"| **Era** | more data |".to_string(),
];
let result = render_table(&table_lines, "", 80);
let texts = rendered_text(&result);
let all_text: String = texts.join(" ");
assert!(all_text.contains("Signature Move"),
"Label was truncated: {}", all_text);
}
#[test]
fn expand_tabs_fixed_4col_boundary_no_tab_returns_no_anchor() {
let (out, anchor) = expand_tabs_with_anchor("hello world");
assert_eq!(out, "hello world");
assert_eq!(anchor, None);
}
#[test]
fn expand_tabs_to_next_tab_stop() {
let (out, anchor) = expand_tabs_with_anchor("ab\tcd");
assert_eq!(out, "ab cd");
assert_eq!(anchor, Some(4));
}
#[test]
fn expand_tabs_anchor_uses_last_tab() {
let (out, anchor) = expand_tabs_with_anchor("a\tb\tc");
assert_eq!(out, "a b c");
assert_eq!(anchor, Some(8));
}
#[test]
fn wrap_continuation_aligns_after_tab_anchor() {
let input = "key:\tlong value with several words that should wrap onto a second line";
let lines = wrap_text(input, 30);
assert!(lines.len() >= 2, "expected wrap, got: {:?}", lines);
for cont in lines.iter().skip(1) {
assert!(
cont.starts_with(" "),
"continuation line not aligned to tab anchor: {:?}",
cont
);
assert!(
!cont.starts_with(" "),
"continuation over-indented: {:?}",
cont
);
}
}
#[test]
fn wrap_falls_back_to_leading_indent_when_no_tab() {
let input = " bullet item with a fairly long description that will need to wrap";
let lines = wrap_text(input, 30);
assert!(lines.len() >= 2);
for cont in lines.iter().skip(1) {
assert!(
cont.starts_with(" "),
"continuation lost leading indent: {:?}",
cont
);
assert!(
!cont.starts_with(" "),
"continuation over-indented: {:?}",
cont
);
}
}
#[test]
fn wrap_clamps_huge_tab_anchor_to_leading_indent() {
let padded = format!(
"{}\tbody content with a few words to force a wrap event",
"x".repeat(50)
);
let lines = wrap_text(&padded, 64);
assert!(lines.len() >= 2);
for cont in lines.iter().skip(1) {
assert!(
!cont.starts_with(" "),
"continuation was over-indented despite safety clamp: {:?}",
cont
);
}
}
}