use super::{DisplayLine, DisplayRole, DisplaySpan};
use std::sync::OnceLock;
use syntect::{
easy::HighlightLines,
highlighting::{Style as SyntectStyle, Theme, ThemeSet},
parsing::{SyntaxReference, SyntaxSet},
};
const MAX_HIGHLIGHT_LINES: usize = 400;
const MAX_HIGHLIGHT_BYTES: usize = 64 * 1024;
struct HighlightAssets {
syntaxes: SyntaxSet,
theme: Theme,
}
static ASSETS: OnceLock<HighlightAssets> = OnceLock::new();
pub(crate) fn prewarm() {
ASSETS.get_or_init(load_assets);
}
pub(crate) fn highlight_code(input: &str, language: Option<&str>) -> Vec<DisplayLine> {
if input.is_empty() {
return vec![DisplayLine::from_span("", DisplayRole::FallbackCode)];
}
let line_count = input.split('\n').count();
if line_count > MAX_HIGHLIGHT_LINES || input.len() > MAX_HIGHLIGHT_BYTES {
return fallback_code(input);
}
let Some(language) = language.filter(|language| !language.trim().is_empty()) else {
return fallback_code(input);
};
let assets = ASSETS.get_or_init(load_assets);
let Some(syntax) = find_syntax(&assets.syntaxes, language) else {
return fallback_code(input);
};
let mut highlighter = HighlightLines::new(syntax, &assets.theme);
input
.split('\n')
.map(|line| highlight_line(line, &mut highlighter, &assets.syntaxes))
.collect()
}
fn load_assets() -> HighlightAssets {
let syntaxes = SyntaxSet::load_defaults_newlines();
let themes = ThemeSet::load_defaults();
let theme = themes
.themes
.get("InspiredGitHub")
.or_else(|| themes.themes.values().next())
.cloned()
.unwrap_or_default();
HighlightAssets { syntaxes, theme }
}
fn find_syntax<'a>(syntaxes: &'a SyntaxSet, language: &str) -> Option<&'a SyntaxReference> {
let language = language.trim().trim_start_matches('.');
syntaxes
.find_syntax_by_token(language)
.or_else(|| syntaxes.find_syntax_by_extension(language))
.or_else(|| syntaxes.find_syntax_by_name(language))
}
fn highlight_line(
line: &str,
highlighter: &mut HighlightLines<'_>,
syntaxes: &SyntaxSet,
) -> DisplayLine {
let highlighted = highlighter.highlight_line(line, syntaxes);
match highlighted {
Ok(ranges) => {
let spans = ranges
.into_iter()
.filter(|(_, text)| !text.is_empty())
.map(|(style, text)| DisplaySpan::new(text, role_for_style(style)))
.collect::<Vec<_>>();
if spans.is_empty() {
DisplayLine::from_span("", DisplayRole::FallbackCode)
} else {
DisplayLine { spans }
}
}
Err(_) => DisplayLine::from_span(line, DisplayRole::FallbackCode),
}
}
fn role_for_style(style: SyntectStyle) -> DisplayRole {
let fg = style.foreground;
if fg.r > 150 && fg.g < 120 && fg.b < 120 {
DisplayRole::String
} else if fg.r < 120 && fg.g < 140 && fg.b < 160 {
DisplayRole::Comment
} else if fg.b > 150 && fg.r < 120 {
DisplayRole::Keyword
} else if fg.b > 150 && fg.r < 170 {
DisplayRole::Function
} else if fg.r > 130 && fg.b > 130 && fg.g < 150 {
DisplayRole::Type
} else if fg.r > 130 && fg.g > 100 && fg.b < 120 {
DisplayRole::Number
} else if fg.r == fg.g && fg.g == fg.b {
DisplayRole::Punctuation
} else if fg.g > fg.r && fg.g > fg.b {
DisplayRole::Operator
} else {
DisplayRole::FallbackCode
}
}
fn fallback_code(input: &str) -> Vec<DisplayLine> {
input
.split('\n')
.map(|line| DisplayLine::from_span(line, DisplayRole::FallbackCode))
.collect()
}
#[cfg(test)]
fn roles_for(input: &str, language: Option<&str>) -> Vec<DisplayRole> {
highlight_code(input, language)
.into_iter()
.flat_map(|line| line.spans)
.map(|span| span.role)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn prewarm_initializes_assets() {
prewarm();
assert!(
ASSETS.get().is_some(),
"ASSETS must be initialized after prewarm()"
);
}
#[test]
fn rust_keyword_uses_a_non_fallback_color_bucket() {
let roles = roles_for("fn", Some("rust"));
assert!(
roles.iter().any(|role| *role != DisplayRole::FallbackCode),
"recognized Rust keyword should receive some theme color bucket"
);
}
#[test]
fn rust_string_literal_uses_a_non_fallback_color_bucket() {
let roles = roles_for("\"hello\"", Some("rust"));
assert!(
roles.iter().any(|role| *role != DisplayRole::FallbackCode),
"recognized Rust string literal should receive some theme color bucket"
);
}
#[test]
fn rust_comment_uses_a_non_fallback_color_bucket() {
let roles = roles_for("// comment", Some("rust"));
assert!(
roles.iter().any(|role| *role != DisplayRole::FallbackCode),
"recognized Rust comment should receive some theme color bucket"
);
}
#[test]
fn highlight_role_mapping_is_theme_color_bucket_not_semantic_scope() {
assert!(
roles_for("fn", Some("rust")).contains(&DisplayRole::String),
"InspiredGitHub colors currently bucket `fn` as String; this is not a semantic keyword assertion"
);
assert!(
roles_for("\"hello\"", Some("rust")).contains(&DisplayRole::Comment),
"InspiredGitHub colors currently bucket string literals as Comment; this is not a semantic string assertion"
);
assert!(
roles_for("// comment", Some("rust")).contains(&DisplayRole::Operator),
"InspiredGitHub colors currently bucket comments as Operator; this is not a semantic comment assertion"
);
}
#[test]
fn recognized_language_uses_non_fallback_roles() {
let lines = highlight_code("fn main() {\n let n = 1;\n}", Some("rust"));
assert!(
lines
.iter()
.flat_map(|line| &line.spans)
.any(|span| span.role != DisplayRole::FallbackCode)
);
}
#[test]
fn unknown_and_missing_language_fall_back() {
for language in [Some("not-a-real-language"), None] {
let lines = highlight_code("let x = 1;", language);
assert!(
lines
.iter()
.flat_map(|line| &line.spans)
.all(|span| span.role == DisplayRole::FallbackCode)
);
}
}
#[test]
fn budget_overflow_falls_back_without_panic() {
let input = (0..=MAX_HIGHLIGHT_LINES)
.map(|_| "fn main() {}")
.collect::<Vec<_>>()
.join("\n");
let lines = highlight_code(&input, Some("rust"));
assert!(
lines
.iter()
.flat_map(|line| &line.spans)
.all(|span| span.role == DisplayRole::FallbackCode)
);
}
}