use std::sync::OnceLock;
use syntect::{
highlighting::Theme,
html::highlighted_html_for_string,
parsing::SyntaxSet,
};
use two_face::{
re_exports::syntect::highlighting::ThemeSet,
theme::{EmbeddedLazyThemeSet, EmbeddedThemeName},
};
use super::{
error::{SyntaxError, SyntaxResult},
types::{SyntaxConfig, SyntaxHighlighter, SyntaxManager},
};
pub struct SyntectHighlighter {
theme_name: String,
}
impl SyntectHighlighter {
pub fn new(theme_name: Option<String>) -> Self {
Self {
theme_name: theme_name.unwrap_or_else(|| "InspiredGitHub".to_string()),
}
}
fn syntax_set() -> &'static SyntaxSet {
static SYNTAX_SET: OnceLock<SyntaxSet> = OnceLock::new();
SYNTAX_SET.get_or_init(two_face::syntax::extra_newlines)
}
fn theme_set() -> &'static EmbeddedLazyThemeSet {
static THEME_SET: OnceLock<EmbeddedLazyThemeSet> = OnceLock::new();
THEME_SET.get_or_init(two_face::theme::extra)
}
fn default_theme_set() -> &'static ThemeSet {
static DEFAULT_THEME_SET: OnceLock<ThemeSet> = OnceLock::new();
DEFAULT_THEME_SET.get_or_init(ThemeSet::load_defaults)
}
fn get_theme(&self, theme_name: Option<&str>) -> &'static Theme {
let theme_set = Self::theme_set();
let default_theme_set = Self::default_theme_set();
let name = if let Some(theme) = theme_name {
theme
} else if !self.theme_name.is_empty() {
&self.theme_name
} else {
"InspiredGitHub" };
if let Some(theme) = default_theme_set.themes.get(name) {
return theme;
}
let embedded_theme = match name {
"Ansi" => Some(EmbeddedThemeName::Ansi),
"Base16" => Some(EmbeddedThemeName::Base16),
"Base16EightiesDark" => Some(EmbeddedThemeName::Base16EightiesDark),
"Base16MochaDark" => Some(EmbeddedThemeName::Base16MochaDark),
"Base16OceanDark" => Some(EmbeddedThemeName::Base16OceanDark),
"Base16OceanLight" => Some(EmbeddedThemeName::Base16OceanLight),
"Base16_256" => Some(EmbeddedThemeName::Base16_256),
"ColdarkCold" => Some(EmbeddedThemeName::ColdarkCold),
"ColdarkDark" => Some(EmbeddedThemeName::ColdarkDark),
"DarkNeon" => Some(EmbeddedThemeName::DarkNeon),
"Dracula" => Some(EmbeddedThemeName::Dracula),
"Github" => Some(EmbeddedThemeName::Github),
"GruvboxDark" => Some(EmbeddedThemeName::GruvboxDark),
"GruvboxLight" => Some(EmbeddedThemeName::GruvboxLight),
"InspiredGithub" => Some(EmbeddedThemeName::InspiredGithub),
"Leet" => Some(EmbeddedThemeName::Leet),
"MonokaiExtended" => Some(EmbeddedThemeName::MonokaiExtended),
"MonokaiExtendedBright" => Some(EmbeddedThemeName::MonokaiExtendedBright),
"MonokaiExtendedLight" => Some(EmbeddedThemeName::MonokaiExtendedLight),
"MonokaiExtendedOrigin" => Some(EmbeddedThemeName::MonokaiExtendedOrigin),
"Nord" => Some(EmbeddedThemeName::Nord),
"OneHalfDark" => Some(EmbeddedThemeName::OneHalfDark),
"OneHalfLight" => Some(EmbeddedThemeName::OneHalfLight),
"SolarizedDark" => Some(EmbeddedThemeName::SolarizedDark),
"SolarizedLight" => Some(EmbeddedThemeName::SolarizedLight),
"SublimeSnazzy" => Some(EmbeddedThemeName::SublimeSnazzy),
"TwoDark" => Some(EmbeddedThemeName::TwoDark),
"VisualStudioDarkPlus" => Some(EmbeddedThemeName::VisualStudioDarkPlus),
"Zenburn" => Some(EmbeddedThemeName::Zenburn),
_ => None,
};
if let Some(embedded_name) = embedded_theme {
return theme_set.get(embedded_name);
}
default_theme_set
.themes
.get("InspiredGitHub")
.unwrap_or_else(|| theme_set.get(EmbeddedThemeName::InspiredGithub))
}
}
impl Default for SyntectHighlighter {
fn default() -> Self {
Self::new(None)
}
}
impl SyntaxHighlighter for SyntectHighlighter {
fn name(&self) -> &'static str {
"Syntect"
}
fn supported_languages(&self) -> Vec<String> {
Self::syntax_set()
.syntaxes()
.iter()
.flat_map(|syntax| {
std::iter::once(syntax.name.to_lowercase())
.chain(syntax.file_extensions.iter().map(|ext| ext.to_lowercase()))
})
.collect()
}
fn available_themes(&self) -> Vec<String> {
let default_theme_set = Self::default_theme_set();
let mut themes: Vec<String> =
default_theme_set.themes.keys().cloned().collect();
let embedded_themes: Vec<String> = vec![
"Ansi".to_string(),
"Base16".to_string(),
"Base16EightiesDark".to_string(),
"Base16MochaDark".to_string(),
"Base16OceanDark".to_string(),
"Base16OceanLight".to_string(),
"Base16_256".to_string(),
"ColdarkCold".to_string(),
"ColdarkDark".to_string(),
"DarkNeon".to_string(),
"Dracula".to_string(),
"Github".to_string(),
"GruvboxDark".to_string(),
"GruvboxLight".to_string(),
"InspiredGithub".to_string(),
"Leet".to_string(),
"MonokaiExtended".to_string(),
"MonokaiExtendedBright".to_string(),
"MonokaiExtendedLight".to_string(),
"MonokaiExtendedOrigin".to_string(),
"Nord".to_string(),
"OneHalfDark".to_string(),
"OneHalfLight".to_string(),
"SolarizedDark".to_string(),
"SolarizedLight".to_string(),
"SublimeSnazzy".to_string(),
"TwoDark".to_string(),
"VisualStudioDarkPlus".to_string(),
"Zenburn".to_string(),
];
themes.extend(embedded_themes);
themes.sort();
themes.dedup();
themes
}
fn highlight(
&self,
code: &str,
language: &str,
theme: Option<&str>,
) -> SyntaxResult<String> {
let syntax_set = Self::syntax_set();
let syntax = syntax_set
.find_syntax_by_token(language)
.unwrap_or_else(|| syntax_set.find_syntax_plain_text());
let theme = self.get_theme(theme);
highlighted_html_for_string(code, syntax_set, syntax, theme)
.map_err(|e| SyntaxError::HighlightingFailed(e.to_string()))
}
fn language_from_extension(&self, extension: &str) -> Option<String> {
let syntax_set = Self::syntax_set();
syntax_set
.find_syntax_by_extension(extension)
.map(|syntax| syntax.name.to_lowercase())
}
}
pub fn create_syntect_manager() -> SyntaxResult<SyntaxManager> {
let highlighter = Box::new(SyntectHighlighter::default());
let mut config = SyntaxConfig::default();
config.default_theme = Some("InspiredGitHub".to_string());
Ok(SyntaxManager::new(highlighter, config))
}