code complexity & repetition analysis tool
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}