use ratatui::{
style::{Color, Modifier, Style},
text::{Line, Span},
};
use crate::util::text::char_width;
use super::MarkdownRenderer;
impl MarkdownRenderer {
pub(super) fn render_single_line_with_number(
&self,
line: &str,
line_idx: usize,
max_width: usize,
) -> Line<'static> {
let line_num = self.format_line_number(line_idx);
let visible_line: String = line.chars().skip(self.horizontal_scroll).collect();
let trimmed = visible_line.trim_start();
let indent_chars = visible_line.chars().count() - trimmed.chars().count();
let indent_width: usize = visible_line
.chars()
.take(indent_chars)
.map(char_width)
.sum();
let indent = " ".repeat(indent_width);
if let Some(stripped) = trimmed.strip_prefix("# ") {
let text = stripped.trim();
return Line::from(vec![
Span::styled(line_num, self.style(Color::DarkGray)),
Span::styled(indent, self.style(self.theme.text_normal)),
Span::styled(format!("◆ {}", text), self.style_bold(self.theme.md_h1)),
]);
}
if let Some(stripped) = trimmed.strip_prefix("## ") {
let text = stripped.trim();
return Line::from(vec![
Span::styled(line_num, self.style(Color::DarkGray)),
Span::styled(indent, self.style(self.theme.text_normal)),
Span::styled(format!("◇ {}", text), self.style_bold(self.theme.md_h2)),
]);
}
if let Some(stripped) = trimmed.strip_prefix("### ") {
let text = stripped.trim();
return Line::from(vec![
Span::styled(line_num, self.style(Color::DarkGray)),
Span::styled(indent, self.style(self.theme.text_normal)),
Span::styled(format!("〈 {} 〉", text), self.style_bold(self.theme.md_h3)),
]);
}
if let Some(stripped) = trimmed.strip_prefix("#### ") {
let text = stripped.trim();
return Line::from(vec![
Span::styled(line_num, self.style(Color::DarkGray)),
Span::styled(indent, self.style(self.theme.text_normal)),
Span::styled(format!("› {} ", text), self.style_bold(self.theme.md_h4)),
]);
}
if trimmed == "---" || trimmed == "***" || trimmed == "___" {
let width = max_width.saturating_sub(indent_width).min(40);
return Line::from(vec![
Span::styled(line_num, self.style(Color::DarkGray)),
Span::styled(indent, self.style(self.theme.text_normal)),
Span::styled("─".repeat(width), self.style(self.theme.text_dim)),
]);
}
if let Some(stripped) = trimmed.strip_prefix("- [ ]") {
let text = stripped.trim();
let rendered = self.render_inline(text);
let mut spans = vec![
Span::styled(line_num, self.style(Color::DarkGray)),
Span::styled(indent, self.style(self.theme.text_normal)),
Span::styled("○ ", self.style(self.theme.text_dim)),
];
spans.extend(rendered);
return Line::from(spans);
}
if trimmed.starts_with("- [x]") || trimmed.starts_with("- [X]") {
let text = trimmed[5..].trim();
let rendered = self.render_inline(text);
let mut spans = vec![
Span::styled(line_num, self.style(Color::DarkGray)),
Span::styled(indent, self.style(self.theme.text_normal)),
Span::styled("● ", self.style(self.theme.md_list_bullet)),
];
spans.extend(rendered);
return Line::from(spans);
}
if trimmed.starts_with("- ") || trimmed.starts_with("* ") {
let text = &trimmed[2..];
let rendered = self.render_inline(text);
let mut spans = vec![
Span::styled(line_num, self.style(Color::DarkGray)),
Span::styled(indent, self.style(self.theme.text_normal)),
Span::styled("• ", self.style(self.theme.text_normal)),
];
spans.extend(rendered);
return Line::from(spans);
}
if let Some(rest) = trimmed.strip_prefix(|c: char| c.is_ascii_digit())
&& let Some(num_end) = rest.find(['.', ')'])
&& (rest.get(num_end..num_end + 2) == Some(". ")
|| rest.get(num_end..num_end + 2) == Some(") "))
{
let num_str = &trimmed[..rest.len() - rest.len() + num_end + 1];
let text = &rest[num_end + 2..];
let rendered = self.render_inline(text);
let mut spans = vec![
Span::styled(line_num, self.style(Color::DarkGray)),
Span::styled(indent, self.style(self.theme.text_normal)),
Span::styled(format!("{} ", num_str), self.style(self.theme.text_normal)),
];
spans.extend(rendered);
return Line::from(spans);
}
if trimmed.starts_with('>') {
let mut level = 0;
let mut rest = trimmed;
while rest.starts_with('>') {
level += 1;
rest = rest[1..].trim_start();
}
let text = rest;
let bar: String = (0..level).map(|_| "▎").collect::<Vec<_>>().join("");
let bar_style = Style::default()
.fg(self.theme.md_blockquote_bar)
.bg(self.theme.md_blockquote_bg)
.add_modifier(Modifier::BOLD);
let text_style = Style::default()
.fg(self.theme.md_blockquote_text)
.bg(self.theme.md_blockquote_bg);
let rendered = self.render_inline(text);
let styled_rendered: Vec<Span<'static>> = rendered
.into_iter()
.map(|span| {
let has_special_bg =
span.style.bg.is_some_and(|bg| bg != self.theme.bg_primary);
if has_special_bg {
span } else {
Span::styled(
span.content,
span.style.fg.map_or(text_style, |fg| {
if fg == self.theme.text_normal {
text_style
} else {
Style::default().fg(fg).bg(self.theme.md_blockquote_bg)
}
}),
)
}
})
.collect();
let mut spans = vec![
Span::styled(line_num, self.style(Color::DarkGray)),
Span::styled(indent, self.style(self.theme.text_normal)),
Span::styled(format!("{} ", bar), bar_style),
];
spans.extend(styled_rendered);
return Line::from(spans);
}
let rendered = self.render_inline(trimmed);
let mut spans = vec![
Span::styled(line_num, self.style(Color::DarkGray)),
Span::styled(indent, self.style(self.theme.text_normal)),
];
spans.extend(rendered);
Line::from(spans)
}
}