1mod languages;
33
34pub use languages::{all_aliases, aliases_for, language_alias, LANGUAGE_ALIASES};
35
36use syntect::easy::HighlightLines;
37use syntect::highlighting::{Color, FontStyle, Style, Theme, ThemeSet};
38use syntect::parsing::{SyntaxReference, SyntaxSet};
39use syntect::util::as_24_bit_terminal_escaped;
40
41const RESET: &str = "\x1b[0m";
43
44pub struct Highlighter {
49 syntax_set: SyntaxSet,
51 theme_set: ThemeSet,
53 theme_name: String,
55 background_override: Option<(u8, u8, u8)>,
57}
58
59impl std::fmt::Debug for Highlighter {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 f.debug_struct("Highlighter")
62 .field("theme_name", &self.theme_name)
63 .field("background_override", &self.background_override)
64 .finish()
65 }
66}
67
68impl Default for Highlighter {
69 fn default() -> Self {
70 Self::new()
71 }
72}
73
74impl Highlighter {
75 pub fn new() -> Self {
77 Self::with_theme("base16-ocean.dark")
78 }
79
80 pub fn with_theme(theme_name: &str) -> Self {
91 Self {
92 syntax_set: SyntaxSet::load_defaults_newlines(),
93 theme_set: ThemeSet::load_defaults(),
94 theme_name: theme_name.to_string(),
95 background_override: None,
96 }
97 }
98
99 pub fn syntax_set(&self) -> &SyntaxSet {
101 &self.syntax_set
102 }
103
104 pub fn theme_set(&self) -> &ThemeSet {
106 &self.theme_set
107 }
108
109 pub fn set_theme(&mut self, theme_name: &str) {
111 self.theme_name = theme_name.to_string();
112 }
113
114 pub fn theme_name(&self) -> &str {
116 &self.theme_name
117 }
118
119 pub fn theme(&self) -> &Theme {
121 self.theme_set
122 .themes
123 .get(&self.theme_name)
124 .unwrap_or_else(|| {
125 self.theme_set
126 .themes
127 .values()
128 .next()
129 .expect("No themes available")
130 })
131 }
132
133 pub fn set_background(&mut self, color: Option<(u8, u8, u8)>) {
147 self.background_override = color;
148 }
149
150 pub fn background(&self) -> Option<(u8, u8, u8)> {
152 self.background_override
153 }
154
155 pub fn syntax_for_language(&self, language: &str) -> Option<&SyntaxReference> {
160 let canonical = language_alias(language);
162
163 if let Some(syntax) = self.syntax_set.find_syntax_by_name(canonical) {
165 return Some(syntax);
166 }
167
168 if let Some(syntax) = self.syntax_set.find_syntax_by_token(canonical) {
170 return Some(syntax);
171 }
172
173 if let Some(syntax) = self.syntax_set.find_syntax_by_extension(canonical) {
175 return Some(syntax);
176 }
177
178 self.syntax_set.find_syntax_by_token(language)
180 }
181
182 pub fn plain_text(&self) -> &SyntaxReference {
184 self.syntax_set.find_syntax_plain_text()
185 }
186
187 pub fn new_highlight_state(&self, language: &str) -> HighlightState<'_> {
191 let syntax = self
192 .syntax_for_language(language)
193 .unwrap_or_else(|| self.plain_text());
194 HighlightState::new(syntax, self.theme())
195 }
196
197 pub fn highlight_line_with_state(&self, line: &str, state: &mut HighlightState) -> String {
205 match state.highlighter.highlight_line(line, &self.syntax_set) {
206 Ok(ranges) => {
207 if self.background_override.is_some() {
208 self.styles_to_ansi(&ranges)
210 } else {
211 let escaped = as_24_bit_terminal_escaped(&ranges, false);
213 format!("{}{}", escaped, RESET)
214 }
215 }
216 Err(_) => line.to_string(), }
218 }
219
220 fn styles_to_ansi(&self, ranges: &[(Style, &str)]) -> String {
222 let mut output = String::new();
223
224 for (style, text) in ranges {
225 if text.is_empty() {
227 continue;
228 }
229
230 let mut codes = Vec::new();
231
232 let fg = style.foreground;
234 codes.push(format!("38;2;{};{};{}", fg.r, fg.g, fg.b));
235
236 if style.font_style.contains(FontStyle::BOLD) {
240 codes.push("1".to_string());
241 }
242 if style.font_style.contains(FontStyle::ITALIC) {
243 codes.push("3".to_string());
244 }
245 if style.font_style.contains(FontStyle::UNDERLINE) {
246 codes.push("4".to_string());
247 }
248
249 if !codes.is_empty() {
251 output.push_str(&format!("\x1b[{}m", codes.join(";")));
252 }
253
254 output.push_str(text);
255 }
256
257 if !output.is_empty() {
259 output.push_str(RESET);
260 }
261
262 output
263 }
264
265 pub fn highlight_block(&self, code: &str, language: &str) -> String {
270 let mut state = self.new_highlight_state(language);
271 let mut output = String::new();
272
273 for line in code.lines() {
274 output.push_str(&self.highlight_line_with_state(line, &mut state));
275 output.push('\n');
276 }
277
278 output
279 }
280
281 pub fn highlight(&self, code: &str, language: Option<&str>) -> String {
285 let lang = language.unwrap_or("text");
286 self.highlight_block(code, lang)
287 }
288
289 pub fn themes(&self) -> Vec<&str> {
291 self.theme_set.themes.keys().map(|s| s.as_str()).collect()
292 }
293
294 pub fn languages(&self) -> Vec<&str> {
296 self.syntax_set
297 .syntaxes()
298 .iter()
299 .map(|s| s.name.as_str())
300 .collect()
301 }
302
303 pub fn has_theme(&self, name: &str) -> bool {
305 self.theme_set.themes.contains_key(name)
306 }
307
308 pub fn has_language(&self, name: &str) -> bool {
310 self.syntax_for_language(name).is_some()
311 }
312}
313
314pub struct HighlightState<'a> {
319 highlighter: HighlightLines<'a>,
321}
322
323impl<'a> HighlightState<'a> {
324 pub fn new(syntax: &'a SyntaxReference, theme: &'a Theme) -> Self {
326 Self {
327 highlighter: HighlightLines::new(syntax, theme),
328 }
329 }
330}
331
332pub fn override_theme_background(theme: &Theme, bg: (u8, u8, u8)) -> Theme {
338 let mut new_theme = theme.clone();
339
340 new_theme.settings.background = Some(Color {
342 r: bg.0,
343 g: bg.1,
344 b: bg.2,
345 a: 255,
346 });
347
348 for item in &mut new_theme.scopes {
350 item.style.background = None;
351 }
352
353 new_theme
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 #[test]
361 fn test_new_highlighter() {
362 let h = Highlighter::new();
363 assert_eq!(h.theme_name(), "base16-ocean.dark");
364 }
365
366 #[test]
367 fn test_with_theme() {
368 let h = Highlighter::with_theme("Solarized (dark)");
369 assert_eq!(h.theme_name(), "Solarized (dark)");
370 }
371
372 #[test]
373 fn test_set_background() {
374 let mut h = Highlighter::new();
375 assert!(h.background().is_none());
376
377 h.set_background(Some((30, 30, 30)));
378 assert_eq!(h.background(), Some((30, 30, 30)));
379
380 h.set_background(None);
381 assert!(h.background().is_none());
382 }
383
384 #[test]
385 fn test_syntax_for_language() {
386 let h = Highlighter::new();
387
388 assert!(h.syntax_for_language("Rust").is_some());
390 assert!(h.syntax_for_language("Python").is_some());
391
392 assert!(h.syntax_for_language("rust").is_some());
394 assert!(h.syntax_for_language("py").is_some());
395 assert!(h.syntax_for_language("js").is_some());
396 assert!(h.syntax_for_language("sh").is_some());
397 assert!(h.syntax_for_language("bash").is_some());
398 assert!(h.syntax_for_language("c").is_some());
399 assert!(h.syntax_for_language("cpp").is_some());
400 }
401
402 #[test]
403 fn test_highlight_block() {
404 let h = Highlighter::new();
405 let code = "fn main() {\n println!(\"Hello\");\n}";
406 let result = h.highlight_block(code, "rust");
407
408 assert!(result.contains("\x1b["));
410 assert!(result.contains("main"));
412 assert!(result.contains("println"));
413 }
414
415 #[test]
416 fn test_highlight_line_streaming() {
417 let h = Highlighter::new();
418 let mut state = h.new_highlight_state("rust");
419
420 let line1 = h.highlight_line_with_state("fn main() {", &mut state);
421 let line2 = h.highlight_line_with_state(" println!(\"Hello\");", &mut state);
422 let line3 = h.highlight_line_with_state("}", &mut state);
423
424 assert!(line1.contains("\x1b["));
426 assert!(line2.contains("\x1b["));
427 assert!(line3.contains("\x1b["));
428 }
429
430 #[test]
431 fn test_themes() {
432 let h = Highlighter::new();
433 let themes = h.themes();
434
435 assert!(!themes.is_empty());
436 assert!(themes.contains(&"base16-ocean.dark"));
437 }
438
439 #[test]
440 fn test_languages() {
441 let h = Highlighter::new();
442 let langs = h.languages();
443
444 assert!(!langs.is_empty());
445 assert!(langs.contains(&"Rust"));
446 assert!(langs.contains(&"Python"));
447 }
448
449 #[test]
450 fn test_has_theme() {
451 let h = Highlighter::new();
452 assert!(h.has_theme("base16-ocean.dark"));
453 assert!(!h.has_theme("nonexistent-theme"));
454 }
455
456 #[test]
457 fn test_has_language() {
458 let h = Highlighter::new();
459 assert!(h.has_language("rust"));
460 assert!(h.has_language("python"));
461 assert!(h.has_language("py")); }
463
464 #[test]
465 fn test_override_theme_background() {
466 let h = Highlighter::new();
467 let theme = h.theme();
468 let new_theme = override_theme_background(theme, (10, 20, 30));
469
470 assert_eq!(
471 new_theme.settings.background,
472 Some(Color { r: 10, g: 20, b: 30, a: 255 })
473 );
474 }
475
476 #[test]
477 fn test_plain_text_fallback() {
478 let h = Highlighter::new();
479 let result = h.highlight_block("just some text", "unknown-lang-xyz");
480
481 assert!(result.contains("just some text"));
483 }
484
485 #[test]
486 fn test_multiline_token() {
487 let h = Highlighter::new();
488 let mut state = h.new_highlight_state("rust");
489
490 let line1 = h.highlight_line_with_state("/* this is a", &mut state);
492 let line2 = h.highlight_line_with_state(" multi-line comment */", &mut state);
493 let line3 = h.highlight_line_with_state("let x = 1;", &mut state);
494
495 assert!(!line1.is_empty());
497 assert!(!line2.is_empty());
498 assert!(!line3.is_empty());
499 }
500
501 #[test]
502 fn test_background_override_styling() {
503 let mut h = Highlighter::new();
504 h.set_background(Some((30, 30, 30)));
505
506 let code = "let x = 1;";
507 let result = h.highlight_block(code, "rust");
508
509 assert!(result.contains("38;2;")); }
514}