use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span, Text};
pub fn parse(input: &str) -> Text<'static> {
let mut lines: Vec<Line<'static>> = Vec::new();
let mut in_code = false;
for raw in input.lines() {
let line = raw.trim_end_matches(['\r']);
if line.trim_start().starts_with("```") {
in_code = !in_code;
lines.push(Line::from(Span::styled(
line.to_string(),
Style::default().fg(Color::DarkGray),
)));
continue;
}
if in_code {
lines.push(Line::from(Span::styled(
line.to_string(),
Style::default().fg(Color::Cyan),
)));
continue;
}
if let Some(rest) = line.strip_prefix("### ") {
lines.push(Line::from(Span::styled(
rest.to_string(),
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)));
continue;
}
if let Some(rest) = line.strip_prefix("## ") {
lines.push(Line::from(Span::styled(
rest.to_string(),
Style::default()
.fg(Color::Magenta)
.add_modifier(Modifier::BOLD),
)));
continue;
}
if let Some(rest) = line.strip_prefix("# ") {
lines.push(Line::from(Span::styled(
rest.to_string(),
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
)));
continue;
}
if line.trim() == "---" || line.trim() == "***" {
lines.push(Line::from(Span::styled(
"────────────────────".to_string(),
Style::default().fg(Color::DarkGray),
)));
continue;
}
let trimmed = line.trim_start();
if trimmed.starts_with('|') && trimmed.matches('|').count() >= 2 {
if trimmed.chars().all(|c| matches!(c, '|' | '-' | ':' | ' ')) {
lines.push(Line::from(Span::styled(
trimmed.to_string(),
Style::default().fg(Color::DarkGray),
)));
} else {
lines.push(Line::from(Span::styled(
trimmed.to_string(),
Style::default().fg(Color::White),
)));
}
continue;
}
if let Some(rest) = list_item(trimmed) {
let indent = line.len() - trimmed.len();
let prefix = format!("{}• ", " ".repeat(indent));
let mut spans = vec![Span::styled(prefix, Style::default().fg(Color::Blue))];
spans.extend(inline(rest));
lines.push(Line::from(spans));
continue;
}
if line.is_empty() {
lines.push(Line::from(String::new()));
} else {
lines.push(Line::from(inline(line)));
}
}
Text::from(lines)
}
fn list_item(trimmed: &str) -> Option<&str> {
for marker in ["- ", "* ", "+ "] {
if let Some(rest) = trimmed.strip_prefix(marker) {
return Some(rest);
}
}
None
}
fn inline(text: &str) -> Vec<Span<'static>> {
let mut spans: Vec<Span<'static>> = Vec::new();
let chars: Vec<char> = text.chars().collect();
let mut i = 0;
let mut plain = String::new();
let flush = |plain: &mut String, spans: &mut Vec<Span<'static>>| {
if !plain.is_empty() {
spans.push(Span::raw(std::mem::take(plain)));
}
};
while i < chars.len() {
if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' {
if let Some(end) = find_delim(&chars, i + 2, "**") {
flush(&mut plain, &mut spans);
let content: String = chars[i + 2..end].iter().collect();
spans.push(Span::styled(
content,
Style::default().add_modifier(Modifier::BOLD),
));
i = end + 2;
continue;
}
}
if chars[i] == '`' {
if let Some(end) = find_char(&chars, i + 1, '`') {
flush(&mut plain, &mut spans);
let content: String = chars[i + 1..end].iter().collect();
spans.push(Span::styled(content, Style::default().fg(Color::Cyan)));
i = end + 1;
continue;
}
}
if chars[i] == '*' {
if let Some(end) = find_char(&chars, i + 1, '*') {
flush(&mut plain, &mut spans);
let content: String = chars[i + 1..end].iter().collect();
spans.push(Span::styled(
content,
Style::default().add_modifier(Modifier::ITALIC),
));
i = end + 1;
continue;
}
}
plain.push(chars[i]);
i += 1;
}
flush(&mut plain, &mut spans);
if spans.is_empty() {
spans.push(Span::raw(String::new()));
}
spans
}
fn find_delim(chars: &[char], start: usize, delim: &str) -> Option<usize> {
let d: Vec<char> = delim.chars().collect();
let mut i = start;
while i + d.len() <= chars.len() {
if chars[i..i + d.len()] == d[..] {
return Some(i);
}
i += 1;
}
None
}
fn find_char(chars: &[char], start: usize, target: char) -> Option<usize> {
(start..chars.len()).find(|&i| chars[i] == target)
}
#[cfg(test)]
mod tests {
use super::*;
fn has_bold(text: &Text) -> bool {
text.lines.iter().any(|l| {
l.spans
.iter()
.any(|s| s.style.add_modifier.contains(Modifier::BOLD))
})
}
#[test]
fn heading_is_bold() {
let t = parse("## Visão técnica");
assert!(has_bold(&t));
}
#[test]
fn plain_line_no_panic() {
let t = parse("uma linha qualquer sem sintaxe especial <>&%");
assert_eq!(t.lines.len(), 1);
}
#[test]
fn list_gets_bullet() {
let t = parse("- item um");
let first = &t.lines[0];
let joined: String = first.spans.iter().map(|s| s.content.as_ref()).collect();
assert!(joined.contains("•"));
}
#[test]
fn bold_inline_detected() {
let t = parse("texto **forte** aqui");
assert!(has_bold(&t));
}
#[test]
fn code_fence_toggles() {
let t = parse("```\ncodigo\n```");
assert_eq!(t.lines.len(), 3);
}
#[test]
fn multiline_count() {
let input = "# Título\n\n## Seção\n- a\n- b\n";
let t = parse(input);
assert_eq!(t.lines.len(), 5);
}
}