use crate::highlight::{highlight_iter, HighlightError, Style};
use crate::languages::Language;
use crate::themes::Theme;
use std::ops::Range;
pub const ANSI_RESET: &str = "\u{1b}[0m";
pub fn hex_to_rgb(hex: &str) -> Option<(u8, u8, u8)> {
let hex = hex.trim_start_matches('#');
if hex.len() != 6 {
return None;
}
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
Some((r, g, b))
}
pub fn rgb_to_ansi(r: u8, g: u8, b: u8, is_background: bool) -> String {
if is_background {
format!("\u{1b}[48;2;{};{};{}m", r, g, b)
} else {
format!("\u{1b}[38;2;{};{};{}m", r, g, b)
}
}
pub fn style_to_ansi(style: &Style) -> String {
let mut result = String::new();
if let Some(fg) = &style.fg {
if let Some((r, g, b)) = hex_to_rgb(fg) {
result.push_str(&rgb_to_ansi(r, g, b, false));
}
}
if let Some(bg) = &style.bg {
if let Some((r, g, b)) = hex_to_rgb(bg) {
result.push_str(&rgb_to_ansi(r, g, b, true));
}
}
if style.bold {
result.push_str("\u{1b}[1m");
}
if style.italic {
result.push_str("\u{1b}[3m");
}
use crate::themes::UnderlineStyle;
match style.text_decoration.underline {
UnderlineStyle::None => {}
UnderlineStyle::Solid => result.push_str("\u{1b}[4m"),
UnderlineStyle::Wavy => result.push_str("\u{1b}[4:3m"),
UnderlineStyle::Double => result.push_str("\u{1b}[4:2m"),
UnderlineStyle::Dotted => result.push_str("\u{1b}[4:4m"),
UnderlineStyle::Dashed => result.push_str("\u{1b}[4:5m"),
}
if style.text_decoration.strikethrough {
result.push_str("\u{1b}[9m");
}
result
}
pub fn paint(text: &str, style: &Style) -> String {
lumis_core::formatter::ansi::paint(text, style)
}
#[deprecated(note = "use `paint(...)` instead")]
pub fn wrap_with_ansi(text: &str, style: &Style) -> String {
paint(text, style)
}
pub struct AnsiIterator {
segments: Vec<(String, Range<usize>)>,
index: usize,
}
impl Iterator for AnsiIterator {
type Item = (String, Range<usize>);
fn next(&mut self) -> Option<Self::Item> {
if self.index < self.segments.len() {
let result = self.segments[self.index].clone();
self.index += 1;
Some(result)
} else {
None
}
}
}
#[deprecated(note = "use `highlight::highlight_iter(...)` with `ansi::paint(...)` instead")]
pub fn highlight_iter_with_ansi(
source: &str,
language: Language,
theme: Option<Theme>,
) -> Result<AnsiIterator, HighlightError> {
let mut segments = Vec::new();
highlight_iter(
source,
language,
theme,
|text, _language, range, _scope, style| {
let wrapped = paint(text, style);
segments.push((wrapped, range));
Ok::<_, std::io::Error>(())
},
)?;
Ok(AnsiIterator { segments, index: 0 })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hex_to_rgb_with_hash() {
assert_eq!(hex_to_rgb("#ff5555"), Some((255, 85, 85)));
assert_eq!(hex_to_rgb("#000000"), Some((0, 0, 0)));
assert_eq!(hex_to_rgb("#ffffff"), Some((255, 255, 255)));
}
#[test]
fn test_hex_to_rgb_without_hash() {
assert_eq!(hex_to_rgb("ff5555"), Some((255, 85, 85)));
assert_eq!(hex_to_rgb("8be9fd"), Some((139, 233, 253)));
}
#[test]
fn test_hex_to_rgb_invalid() {
assert_eq!(hex_to_rgb("invalid"), None);
assert_eq!(hex_to_rgb("#fff"), None);
assert_eq!(hex_to_rgb(""), None);
assert_eq!(hex_to_rgb("#gggggg"), None);
}
#[test]
fn test_rgb_to_ansi_foreground() {
let result = rgb_to_ansi(255, 85, 85, false);
assert_eq!(result, "\u{1b}[38;2;255;85;85m");
}
#[test]
fn test_rgb_to_ansi_background() {
let result = rgb_to_ansi(40, 42, 54, true);
assert_eq!(result, "\u{1b}[48;2;40;42;54m");
}
#[test]
fn test_style_to_ansi_with_fg_only() {
let style = Style {
fg: Some("#ff79c6".to_string()),
..Default::default()
};
let result = style_to_ansi(&style);
assert!(result.contains("\u{1b}[38;2;255;121;198m"));
}
#[test]
fn test_style_to_ansi_with_bold() {
let style = Style {
bold: true,
..Default::default()
};
let result = style_to_ansi(&style);
assert_eq!(result, "\u{1b}[1m");
}
#[test]
fn test_style_to_ansi_with_multiple() {
let style = Style {
fg: Some("#ff5555".to_string()),
bold: true,
italic: true,
..Default::default()
};
let result = style_to_ansi(&style);
assert!(result.contains("\u{1b}[38;2;255;85;85m"));
assert!(result.contains("\u{1b}[1m"));
assert!(result.contains("\u{1b}[3m"));
}
#[test]
fn test_paint() {
let style = Style {
fg: Some("#8be9fd".to_string()),
..Default::default()
};
let result = paint("fn", &style);
assert!(result.starts_with("\u{1b}[0m\u{1b}[38;2;139;233;253m"));
assert!(result.contains("fn"));
assert!(result.ends_with("\u{1b}[0m"));
}
#[test]
fn test_paint_empty_style() {
let style = Style::default();
let result = paint("text", &style);
assert_eq!(result, "text");
}
}