code complexity & repetition analysis tool
at main 235 lines 6.7 kB view raw
1use owo_colors::OwoColorize; 2use syntect::easy::HighlightLines; 3use syntect::highlighting::{Color, Style, ThemeSet}; 4use syntect::parsing::SyntaxSet; 5use syntect::util::LinesWithEndings; 6 7pub struct Highlighter { 8 syntax_set: SyntaxSet, 9 theme_set: ThemeSet, 10} 11 12impl Highlighter { 13 pub fn new() -> Self { 14 Self { syntax_set: SyntaxSet::load_defaults_newlines(), theme_set: ThemeSet::load_defaults() } 15 } 16 17 /// Highlight code with syntax highlighting 18 pub fn highlight(&self, code: &str, file_extension: &str) -> String { 19 let syntax = self 20 .syntax_set 21 .find_syntax_by_extension(file_extension) 22 .unwrap_or_else(|| self.syntax_set.find_syntax_plain_text()); 23 24 let theme = &self.theme_set.themes["base16-ocean.dark"]; 25 26 let mut highlighter = HighlightLines::new(syntax, theme); 27 let mut output = String::new(); 28 29 for line in LinesWithEndings::from(code) { 30 let ranges = highlighter.highlight_line(line, &self.syntax_set).unwrap_or_default(); 31 32 for (style, text) in ranges { 33 output.push_str(&style_to_owo(&style, text)); 34 } 35 } 36 37 output 38 } 39} 40 41impl Default for Highlighter { 42 fn default() -> Self { 43 Self::new() 44 } 45} 46 47/// Convert syntect Style to owo-colors styled text 48fn style_to_owo(style: &Style, text: &str) -> String { 49 let fg = style.foreground; 50 51 let colored = if is_grayscale(fg) { 52 if fg.r < 100 { 53 text.bright_black().to_string() 54 } else if fg.r < 180 { 55 text.white().to_string() 56 } else { 57 text.bright_white().to_string() 58 } 59 } else { 60 match dominant_color(fg) { 61 ColorChannel::Red => { 62 if fg.r > 200 { 63 text.bright_red().to_string() 64 } else { 65 text.red().to_string() 66 } 67 } 68 ColorChannel::Green => { 69 if fg.g > 200 { 70 text.bright_green().to_string() 71 } else { 72 text.green().to_string() 73 } 74 } 75 ColorChannel::Blue => { 76 if fg.b > 200 { 77 text.bright_cyan().to_string() 78 } else { 79 text.cyan().to_string() 80 } 81 } 82 ColorChannel::Yellow => { 83 if fg.r > 200 && fg.g > 200 { 84 text.bright_yellow().to_string() 85 } else { 86 text.yellow().to_string() 87 } 88 } 89 ColorChannel::Magenta => { 90 if fg.r > 200 && fg.b > 200 { 91 text.bright_magenta().to_string() 92 } else { 93 text.magenta().to_string() 94 } 95 } 96 } 97 }; 98 99 if style.font_style.contains(syntect::highlighting::FontStyle::BOLD) { 100 colored.bold().to_string() 101 } else { 102 colored 103 } 104} 105 106enum ColorChannel { 107 Red, 108 Green, 109 Blue, 110 Yellow, 111 Magenta, 112} 113 114fn is_grayscale(color: Color) -> bool { 115 let max_diff = color.r.abs_diff(color.g).max(color.g.abs_diff(color.b)); 116 max_diff < 30 117} 118 119fn dominant_color(color: Color) -> ColorChannel { 120 let r = color.r as u16; 121 let g = color.g as u16; 122 let b = color.b as u16; 123 124 if r > 150 && g > 150 && b < 100 { 125 return ColorChannel::Yellow; 126 } 127 if r > 150 && b > 150 && g < 100 { 128 return ColorChannel::Magenta; 129 } 130 131 if r >= g && r >= b { 132 ColorChannel::Red 133 } else if g >= r && g >= b { 134 ColorChannel::Green 135 } else { 136 ColorChannel::Blue 137 } 138} 139 140#[cfg(test)] 141mod tests { 142 use super::*; 143 144 #[test] 145 fn test_highlighter_creation() { 146 let highlighter = Highlighter::new(); 147 assert!(!highlighter.syntax_set.syntaxes().is_empty()); 148 assert!(!highlighter.theme_set.themes.is_empty()); 149 } 150 151 #[test] 152 fn test_default_highlighter() { 153 let highlighter = Highlighter::default(); 154 assert!(!highlighter.syntax_set.syntaxes().is_empty()); 155 } 156 157 #[test] 158 fn test_highlight_rust_code() { 159 let highlighter = Highlighter::new(); 160 let code = "fn main() {\n println!(\"Hello, world!\");\n}"; 161 let highlighted = highlighter.highlight(code, "rs"); 162 163 assert!(!highlighted.is_empty()); 164 assert!(highlighted.len() >= code.len()); 165 } 166 167 #[test] 168 fn test_highlight_python_code() { 169 let highlighter = Highlighter::new(); 170 let code = "def hello():\n print('Hello, world!')"; 171 let highlighted = highlighter.highlight(code, "py"); 172 173 assert!(!highlighted.is_empty()); 174 assert!(highlighted.len() >= code.len()); 175 } 176 177 #[test] 178 fn test_highlight_unknown_extension() { 179 let highlighter = Highlighter::new(); 180 let code = "some random text"; 181 let highlighted = highlighter.highlight(code, "unknown_ext"); 182 183 assert!(!highlighted.is_empty()); 184 } 185 186 #[test] 187 fn test_is_grayscale() { 188 assert!(is_grayscale(Color { r: 128, g: 128, b: 128, a: 255 })); 189 assert!(is_grayscale(Color { r: 100, g: 105, b: 100, a: 255 })); 190 assert!(!is_grayscale(Color { r: 255, g: 0, b: 0, a: 255 })); 191 assert!(!is_grayscale(Color { r: 200, g: 100, b: 50, a: 255 })); 192 } 193 194 #[test] 195 fn test_dominant_color_red() { 196 let color = Color { r: 255, g: 50, b: 50, a: 255 }; 197 matches!(dominant_color(color), ColorChannel::Red); 198 } 199 200 #[test] 201 fn test_dominant_color_green() { 202 let color = Color { r: 50, g: 255, b: 50, a: 255 }; 203 matches!(dominant_color(color), ColorChannel::Green); 204 } 205 206 #[test] 207 fn test_dominant_color_blue() { 208 let color = Color { r: 50, g: 50, b: 255, a: 255 }; 209 matches!(dominant_color(color), ColorChannel::Blue); 210 } 211 212 #[test] 213 fn test_dominant_color_yellow() { 214 let color = Color { r: 200, g: 200, b: 50, a: 255 }; 215 matches!(dominant_color(color), ColorChannel::Yellow); 216 } 217 218 #[test] 219 fn test_dominant_color_magenta() { 220 let color = Color { r: 200, g: 50, b: 200, a: 255 }; 221 matches!(dominant_color(color), ColorChannel::Magenta); 222 } 223 224 #[test] 225 fn test_style_to_owo_preserves_text() { 226 let style = Style { 227 foreground: Color { r: 255, g: 255, b: 255, a: 255 }, 228 background: Color { r: 0, g: 0, b: 0, a: 255 }, 229 font_style: syntect::highlighting::FontStyle::empty(), 230 }; 231 let text = "test text"; 232 let styled = style_to_owo(&style, text); 233 assert!(styled.contains(text)); 234 } 235}