#![cfg(feature = "syntect")]
use std::sync::LazyLock;
use syntect::easy::HighlightLines;
use syntect::highlighting::{Style, ThemeSet};
use syntect::parsing::SyntaxSet;
use syntect::util::{as_24_bit_terminal_escaped, LinesWithEndings};
static SYNTAX_SET: LazyLock<SyntaxSet> = LazyLock::new(SyntaxSet::load_defaults_newlines);
static THEME_SET: LazyLock<ThemeSet> = LazyLock::new(ThemeSet::load_defaults);
#[derive(Debug, Clone, Copy)]
pub enum Language {
Rust,
Python,
Go,
TypeScript,
JavaScript,
Markdown,
Toml,
Json,
Shell,
}
impl Language {
fn syntax_name(&self) -> &'static str {
match self {
Language::Rust => "Rust",
Language::Python => "Python",
Language::Go => "Go",
Language::TypeScript => "TypeScript",
Language::JavaScript => "JavaScript",
Language::Markdown => "Markdown",
Language::Toml => "TOML",
Language::Json => "JSON",
Language::Shell => "Bourne Again Shell (bash)",
}
}
pub fn from_extension(ext: &str) -> Option<Self> {
match ext.to_lowercase().as_str() {
"rs" => Some(Language::Rust),
"py" => Some(Language::Python),
"go" => Some(Language::Go),
"ts" | "tsx" => Some(Language::TypeScript),
"js" | "jsx" => Some(Language::JavaScript),
"md" => Some(Language::Markdown),
"toml" => Some(Language::Toml),
"json" => Some(Language::Json),
"sh" | "bash" => Some(Language::Shell),
_ => None,
}
}
}
pub fn highlight_code(code: &str, lang: Language) -> String {
let syntax = SYNTAX_SET
.find_syntax_by_name(lang.syntax_name())
.unwrap_or_else(|| SYNTAX_SET.find_syntax_plain_text());
let theme = &THEME_SET.themes["base16-ocean.dark"];
let mut highlighter = HighlightLines::new(syntax, theme);
let mut output = String::new();
for line in LinesWithEndings::from(code) {
let ranges: Vec<(Style, &str)> =
highlighter.highlight_line(line, &SYNTAX_SET).unwrap_or_default();
let escaped = as_24_bit_terminal_escaped(&ranges, false);
output.push_str(&escaped);
}
output.push_str("\x1b[0m");
output
}
pub fn highlight_line(line: &str, lang: Language) -> String {
let syntax = SYNTAX_SET
.find_syntax_by_name(lang.syntax_name())
.unwrap_or_else(|| SYNTAX_SET.find_syntax_plain_text());
let theme = &THEME_SET.themes["base16-ocean.dark"];
let mut highlighter = HighlightLines::new(syntax, theme);
let ranges: Vec<(Style, &str)> =
highlighter.highlight_line(line, &SYNTAX_SET).unwrap_or_default();
let mut escaped = as_24_bit_terminal_escaped(&ranges, false);
escaped.push_str("\x1b[0m");
escaped.trim_end().to_string()
}
pub fn highlight_rust(code: &str) -> String {
highlight_code(code, Language::Rust)
}
pub fn highlight_rust_line(line: &str) -> String {
highlight_line(line, Language::Rust)
}
pub fn print_highlighted(code: &str, lang: Language, indent: &str) {
let syntax = SYNTAX_SET
.find_syntax_by_name(lang.syntax_name())
.unwrap_or_else(|| SYNTAX_SET.find_syntax_plain_text());
let theme = &THEME_SET.themes["base16-ocean.dark"];
let mut highlighter = HighlightLines::new(syntax, theme);
for line in LinesWithEndings::from(code) {
let ranges: Vec<(Style, &str)> =
highlighter.highlight_line(line, &SYNTAX_SET).unwrap_or_default();
let escaped = as_24_bit_terminal_escaped(&ranges, false);
print!("{}{}", indent, escaped);
}
print!("\x1b[0m");
}
pub fn print_highlighted_line(line: &str, lang: Language, indent: &str) {
let highlighted = highlight_line(line, lang);
println!("{}{}", indent, highlighted);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_highlight_rust_code() {
let code = r#"fn main() {
println!("Hello, world!");
}"#;
let highlighted = highlight_rust(code);
assert!(highlighted.contains("\x1b["));
assert!(highlighted.ends_with("\x1b[0m"));
}
#[test]
fn test_highlight_rust_line() {
let line = "let x = 42;";
let highlighted = highlight_rust_line(line);
assert!(highlighted.contains("\x1b["));
}
#[test]
fn test_language_from_extension() {
assert!(matches!(Language::from_extension("rs"), Some(Language::Rust)));
assert!(matches!(Language::from_extension("py"), Some(Language::Python)));
assert!(matches!(Language::from_extension("go"), Some(Language::Go)));
assert!(matches!(Language::from_extension("ts"), Some(Language::TypeScript)));
assert!(Language::from_extension("xyz").is_none());
}
}