use std::sync::LazyLock;
use syntect::html::{ClassStyle, ClassedHTMLGenerator};
use syntect::parsing::SyntaxSet;
use syntect::util::LinesWithEndings;
static SYNTAX_SET: LazyLock<SyntaxSet> = LazyLock::new(SyntaxSet::load_defaults_newlines);
pub fn highlight_code(code: &str, language: Option<&str>) -> String {
let lang = language.unwrap_or("txt");
let syntax_name = map_language(lang);
let syntax = SYNTAX_SET
.find_syntax_by_extension(syntax_name)
.or_else(|| SYNTAX_SET.find_syntax_by_name(syntax_name))
.or_else(|| SYNTAX_SET.find_syntax_by_extension(lang))
.or_else(|| SYNTAX_SET.find_syntax_by_name(lang))
.unwrap_or_else(|| SYNTAX_SET.find_syntax_plain_text());
let mut generator = ClassedHTMLGenerator::new_with_class_style(
syntax,
&SYNTAX_SET,
ClassStyle::SpacedPrefixed { prefix: "sy-" },
);
for line in LinesWithEndings::from(code) {
if generator
.parse_html_for_line_which_includes_newline(line)
.is_err()
{
return escape_html(code);
}
}
generator.finalize()
}
pub fn syntax_highlight_css() -> &'static str {
include_str!("syntax_highlight.css")
}
fn map_language(lang: &str) -> &str {
if lang.eq_ignore_ascii_case("js") || lang.eq_ignore_ascii_case("javascript") {
return "JavaScript";
}
if lang.eq_ignore_ascii_case("jsx") {
return "JavaScript (JSX)";
}
if lang.eq_ignore_ascii_case("ts") || lang.eq_ignore_ascii_case("typescript") {
return "TypeScript";
}
if lang.eq_ignore_ascii_case("tsx") {
return "TypeScript (TSX)";
}
if lang.eq_ignore_ascii_case("sh")
|| lang.eq_ignore_ascii_case("bash")
|| lang.eq_ignore_ascii_case("shell")
|| lang.eq_ignore_ascii_case("zsh")
{
return "Bash";
}
if lang.eq_ignore_ascii_case("rs") || lang.eq_ignore_ascii_case("rust") {
return "Rust";
}
if lang.eq_ignore_ascii_case("py") || lang.eq_ignore_ascii_case("python") {
return "Python";
}
if lang.eq_ignore_ascii_case("rb") || lang.eq_ignore_ascii_case("ruby") {
return "Ruby";
}
if lang.eq_ignore_ascii_case("go") || lang.eq_ignore_ascii_case("golang") {
return "Go";
}
if lang.eq_ignore_ascii_case("json") || lang.eq_ignore_ascii_case("jsonc") {
return "JSON";
}
if lang.eq_ignore_ascii_case("yml") || lang.eq_ignore_ascii_case("yaml") {
return "YAML";
}
if lang.eq_ignore_ascii_case("html") || lang.eq_ignore_ascii_case("htm") {
return "HTML";
}
if lang.eq_ignore_ascii_case("css") {
return "CSS";
}
if lang.eq_ignore_ascii_case("scss") {
return "SCSS";
}
if lang.eq_ignore_ascii_case("sass") {
return "Sass";
}
if lang.eq_ignore_ascii_case("toml") {
return "TOML";
}
if lang.eq_ignore_ascii_case("ini") {
return "INI";
}
if lang.eq_ignore_ascii_case("env") {
return "Bourne Again Shell (bash)";
}
if lang.eq_ignore_ascii_case("md") || lang.eq_ignore_ascii_case("markdown") {
return "Markdown";
}
if lang.eq_ignore_ascii_case("sql") {
return "SQL";
}
if lang.eq_ignore_ascii_case("c") || lang.eq_ignore_ascii_case("h") {
return "C";
}
if lang.eq_ignore_ascii_case("cpp")
|| lang.eq_ignore_ascii_case("cc")
|| lang.eq_ignore_ascii_case("cxx")
|| lang.eq_ignore_ascii_case("hpp")
{
return "C++";
}
if lang.eq_ignore_ascii_case("java") {
return "Java";
}
if lang.eq_ignore_ascii_case("cs") || lang.eq_ignore_ascii_case("csharp") {
return "C#";
}
if lang.eq_ignore_ascii_case("php") {
return "PHP";
}
if lang.eq_ignore_ascii_case("swift") {
return "Swift";
}
if lang.eq_ignore_ascii_case("kt") || lang.eq_ignore_ascii_case("kotlin") {
return "Kotlin";
}
if lang.eq_ignore_ascii_case("dockerfile") || lang.eq_ignore_ascii_case("docker") {
return "Dockerfile";
}
if lang.eq_ignore_ascii_case("txt") || lang.eq_ignore_ascii_case("text") {
return "Plain Text";
}
lang
}
fn escape_html(text: &str) -> String {
text.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_highlight_rust() {
let code = r#"fn main() {
println!("Hello, world!");
}"#;
let html = highlight_code(code, Some("rust"));
assert!(html.contains("<span"));
assert!(html.contains("sy-"));
assert!(html.contains("fn"));
}
#[test]
fn test_highlight_javascript() {
let code = "const x = 42;";
let html = highlight_code(code, Some("js"));
assert!(html.contains("<span"));
}
#[test]
fn test_highlight_no_inline_styles() {
let code = "let x = 42;";
let html = highlight_code(code, Some("rust"));
assert!(!html.contains("style="));
}
#[test]
fn test_highlight_unknown_language() {
let code = "some text";
let html = highlight_code(code, Some("unknown_lang_xyz"));
assert!(!html.is_empty());
}
#[test]
fn test_highlight_no_language() {
let code = "plain text";
let html = highlight_code(code, None);
assert!(!html.is_empty());
}
#[test]
fn test_escape_html() {
assert_eq!(escape_html("<div>"), "<div>");
assert_eq!(escape_html("a & b"), "a & b");
}
}