use ratatui::style::{Modifier, Style};
use ratatui::text::Span;
pub fn inline_spans(text: &str, base: Style, code: Style) -> Vec<Span<'static>> {
let bold = base.add_modifier(Modifier::BOLD);
let italic = base.add_modifier(Modifier::ITALIC);
let bytes = text.as_bytes();
let len = bytes.len();
let mut spans: Vec<Span<'static>> = Vec::new();
let mut seg_start = 0usize; let mut i = 0usize;
macro_rules! flush {
($end:expr) => {
if $end > seg_start {
spans.push(Span::styled(text[seg_start..$end].to_string(), base));
}
};
}
while i < len {
let b = bytes[i];
if b == b'`'
&& let Some(close) = find_byte(bytes, i + 1, b'`')
{
flush!(i);
spans.push(Span::styled(text[i + 1..close].to_string(), code));
i = close + 1;
seg_start = i;
continue;
}
if b == b'*'
&& i + 1 < len
&& bytes[i + 1] == b'*'
&& let Some(close) = find_str(text, i + 2, "**")
{
flush!(i);
spans.push(Span::styled(text[i + 2..close].to_string(), bold));
i = close + 2;
seg_start = i;
continue;
}
if b == b'_'
&& i + 1 < len
&& bytes[i + 1] == b'_'
&& let Some(close) = find_str(text, i + 2, "__")
{
flush!(i);
spans.push(Span::styled(text[i + 2..close].to_string(), bold));
i = close + 2;
seg_start = i;
continue;
}
if b == b'*'
&& i + 1 < len
&& bytes[i + 1] != b'*'
&& let Some(close) = find_byte(bytes, i + 1, b'*')
{
flush!(i);
spans.push(Span::styled(text[i + 1..close].to_string(), italic));
i = close + 1;
seg_start = i;
continue;
}
i += 1;
}
flush!(len);
if spans.is_empty() {
spans.push(Span::styled(text.to_string(), base));
}
spans
}
fn find_byte(bytes: &[u8], start: usize, marker: u8) -> Option<usize> {
bytes[start..]
.iter()
.position(|&b| b == marker)
.map(|pos| start + pos)
}
fn find_str(text: &str, start: usize, marker: &str) -> Option<usize> {
text[start..]
.find(marker)
.map(|rel| start + rel)
.filter(|&pos| pos > start) }
#[cfg(test)]
mod tests {
use super::*;
use ratatui::style::{Color, Style};
fn plain() -> Style {
Style::default()
}
fn code_style() -> Style {
Style::default().fg(Color::Cyan)
}
fn content(spans: &[Span]) -> String {
spans.iter().map(|s| s.content.as_ref()).collect()
}
#[test]
fn plain_text_unchanged() {
let spans = inline_spans("hello world", plain(), code_style());
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].content, "hello world");
}
#[test]
fn backtick_code_span() {
let spans = inline_spans("use `cargo build` here", plain(), code_style());
let text = content(&spans);
assert_eq!(text, "use cargo build here");
let code_span = spans.iter().find(|s| s.content == "cargo build").unwrap();
assert_eq!(code_span.style.fg, Some(Color::Cyan));
}
#[test]
fn double_star_bold() {
let spans = inline_spans("this is **very** important", plain(), code_style());
let bold_span = spans.iter().find(|s| s.content == "very").unwrap();
assert!(bold_span.style.add_modifier.contains(Modifier::BOLD));
}
#[test]
fn single_star_italic() {
let spans = inline_spans("read *carefully*", plain(), code_style());
let it = spans.iter().find(|s| s.content == "carefully").unwrap();
assert!(it.style.add_modifier.contains(Modifier::ITALIC));
}
#[test]
fn mixed_inline() {
let spans = inline_spans("**bold** and `code`", plain(), code_style());
assert!(spans.iter().any(|s| s.content == "bold"));
assert!(spans.iter().any(|s| s.content == "code"));
}
#[test]
fn cjk_text_preserved() {
let spans = inline_spans("中文 **加粗** 结束", plain(), code_style());
let text = content(&spans);
assert_eq!(text, "中文 加粗 结束");
let bold_span = spans.iter().find(|s| s.content == "加粗").unwrap();
assert!(bold_span.style.add_modifier.contains(Modifier::BOLD));
}
}