//! 语法高亮模块。 //! //! 仅在 `server` feature 启用时可用,使用 `syntect` 将代码块转换为带 CSS class 的 HTML, //! 配合 `public/highlight.css` 中生成的主题规则实现亮/暗主题高亮。 #[cfg(feature = "server")] pub mod server { use std::sync::LazyLock; use syntect::html::{ClassStyle, ClassedHTMLGenerator}; use syntect::parsing::SyntaxSet; use syntect::util::LinesWithEndings; /// 全局语法集合,懒加载时合并内置语法与 `syntaxes/` 目录下的自定义语法。 static SYNTAX_SET: LazyLock = LazyLock::new(|| { let mut builder = SyntaxSet::load_defaults_newlines().into_builder(); // 使用 CARGO_MANIFEST_DIR 派生的绝对路径,避免运行时工作目录不确定导致加载失败 let syntaxes_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/syntaxes"); tracing::info!("Loading custom syntaxes from: {}", syntaxes_dir); match builder.add_from_folder(syntaxes_dir, true) { Ok(()) => tracing::info!("Custom syntaxes loaded successfully"), Err(e) => tracing::warn!("Failed to load custom syntaxes: {:?}", e), } let built = builder.build(); tracing::info!( "SyntaxSet built: {} syntaxes, swift={:?}", built.syntaxes().len(), built.find_syntax_by_extension("swift").map(|s| &s.name) ); built }); /// 根据语言标识查找对应的语法定义。 /// /// 依次尝试:扩展名、语法名称、小写扩展名/名称、常用别名映射。 /// 如果全部失败,则回退到纯文本语法。 fn find_syntax(lang: Option<&str>) -> &'static syntect::parsing::SyntaxReference { let ss = &*SYNTAX_SET; if let Some(lang) = lang { if !lang.is_empty() { // 尝试按扩展名匹配 if let Some(s) = ss.find_syntax_by_extension(lang) { return s; } // 尝试按语法名称匹配 if let Some(s) = ss.find_syntax_by_name(lang) { return s; } // 小写扩展名再匹配一次(部分语言的扩展名习惯小写) let lower = lang.to_lowercase(); if lower != lang { if let Some(s) = ss.find_syntax_by_extension(&lower) { return s; } } // 大小写不敏感的语法名称匹配(syntect 的语法名通常首字母大写,如 Haskell) if let Some(s) = ss .syntaxes() .iter() .find(|s| s.name.eq_ignore_ascii_case(lang)) { return s; } // 常用语言别名映射表 let aliases: &[(&str, &str)] = &[ ("rust", "rs"), ("js", "js"), ("javascript", "js"), ("typescript", "ts"), ("py", "py"), ("python", "py"), ("rb", "rb"), ("ruby", "rb"), ("sh", "sh"), ("bash", "sh"), ("yaml", "yaml"), ("yml", "yaml"), ("md", "md"), ("markdown", "md"), ("kotlin", "kt"), ("swift", "swift"), ("golang", "go"), ]; for &(from, to) in aliases { // 别名比较同样不区分大小写,保证 "RUST" 与 "rust" 等价。 if lang.eq_ignore_ascii_case(from) { if let Some(s) = ss.find_syntax_by_extension(to) { return s; } } } } } ss.find_syntax_by_extension("txt") .or_else(|| ss.find_syntax_by_name("Plain Text")) .expect("no plain text syntax") } /// 对给定代码字符串按指定语言进行高亮,返回 HTML 字符串。 /// /// 输出使用 spaced CSS class 风格,便于与 `highlight.css` 中的选择器匹配。 pub fn highlight_code(code: &str, lang: Option<&str>) -> String { let trimmed = code.trim(); let syntax = find_syntax(lang); let ss = &*SYNTAX_SET; let mut generator = ClassedHTMLGenerator::new_with_class_style(syntax, ss, ClassStyle::Spaced); // 逐行解析,出错时记录警告并继续 for line in LinesWithEndings::from(trimmed) { if let Err(e) = generator.parse_html_for_line_which_includes_newline(line) { tracing::warn!("syntect parse error: {:?}", e); } } generator.finalize() } } #[cfg(all(test, feature = "server"))] mod tests { use super::server::*; #[test] fn highlight_code_rust() { let result = highlight_code("fn main() {}", Some("rust")); assert!(result.contains(r#"fn"#)); assert!(result.contains(r#"main"#)); } #[test] fn highlight_code_javascript_alias() { let result = highlight_code("console.log('hi')", Some("js")); assert!(result.contains(r#"console"#)); assert!(result.contains(r#"log"#)); } #[test] fn highlight_code_python_alias() { let result = highlight_code("print('hi')", Some("python")); assert!(result.contains(r#"print"#)); } #[test] fn highlight_code_unknown_language() { let result = highlight_code("some text", Some("brainfuck")); assert!(result.contains(r#"some text"#)); } #[test] fn highlight_code_none_language() { let result = highlight_code("plain text", None); assert!(result.contains(r#"plain text"#)); } #[test] fn highlight_code_empty() { let result = highlight_code("", None); assert!(result.is_empty()); } #[test] fn highlight_code_produces_span_tags() { let result = highlight_code("let x = 1;", Some("rust")); assert!(result.contains(r#"let"#)); assert!(result.contains(r#"1"#)); } #[test] fn highlight_code_haskell_by_full_name() { // Haskell 语法名首字母大写,扩展名为 hs;直接写 "haskell" 应能匹配。 let code = "factorial :: Integer -> Integer\nfactorial 0 = 1"; let result = highlight_code(code, Some("haskell")); assert!( !result.contains(r#""#), "Haskell 不应回退到纯文本: {}", result ); assert!( result.contains("source haskell"), "Haskell 应输出 source haskell: {}", result ); } #[test] fn highlight_code_uppercase_language_falls_back_via_lowercase() { // 大写语言标识应通过小写回退路径匹配到对应语法。 let lower = highlight_code("fn main() {}", Some("rust")); let upper = highlight_code("fn main() {}", Some("RUST")); // 大写标识的输出必须与小写标识完全一致,证明回退路径生效。 assert_eq!(lower, upper); assert!(lower.contains(r#"fn"#)); } #[test] fn highlight_code_resolves_golang_alias() { // 别名表中 "golang" 映射到 "go" 扩展名,输出应与直接用 "go" 一致。 let by_alias = highlight_code("package main", Some("golang")); let by_ext = highlight_code("package main", Some("go")); assert_eq!(by_alias, by_ext); // 别名解析必须产出带 span 的高亮输出,而非纯文本。 assert!(by_alias.contains("span")); } #[test] fn highlight_code_resolves_bash_alias() { // 别名表中 "bash" 映射到 "sh" 扩展名。 let result = highlight_code("echo hello", Some("bash")); assert!(result.contains("span")); } #[test] fn highlight_code_resolves_yml_alias() { // 别名表中 "yml" 映射到 "yaml" 扩展名。 let result = highlight_code("key: value", Some("yml")); assert!(!result.is_empty()); } #[test] fn highlight_code_unknown_language_falls_back_to_plain_text() { // 无法识别的语言应回退到纯文本语法,仍能输出内容。 let result = highlight_code("hello world", Some("totally-not-a-language-xyz")); assert!(result.contains("hello world")); } #[test] fn highlight_code_empty_language_string_falls_back_to_plain_text() { // 空字符串语言标识应走纯文本回退路径。 let result = highlight_code("just text", Some("")); assert!(result.contains("just text")); } #[test] fn highlight_code_trims_surrounding_whitespace() { // 代码首尾的空白会被 trim 掉再高亮。 let result = highlight_code(" \nfn main() {}\n ", Some("rust")); assert!(result.contains(r#"fn"#)); } #[test] fn highlight_code_multiline_output_spans_all_lines() { // 多行代码每一行都应被解析为带 span 的输出。 let code = "fn a() {}\nfn b() {}"; let result = highlight_code(code, Some("rust")); // 两处 fn 关键字都应出现 assert_eq!( result .matches(r#"fn"#) .count(), 2 ); } #[test] fn highlight_code_swift_keyword_and_func() { // Swift 关键字 func/import/let 应生成 declaration/keyword span,而不是纯文本。 let code = "import Foundation\nfunc greet(person: String) -> String {\n return \"Hi\"\n}"; let result = highlight_code(code, Some("swift")); assert!( result.contains("keyword"), "Swift 输出缺少关键字高亮: {}", result ); // 函数名应被识别为函数(声明名 entity name function 或调用 variable function)。 assert!( result.contains("name function") || result.contains("variable function"), "Swift func 名缺少函数高亮: {}", result ); } #[test] fn highlight_code_swift_types_and_strings() { // Swift 标准库类型与字符串字面量都应被识别。 let code = "let count: Int = 42\nlet name = \"hello\""; let result = highlight_code(code, Some("swift")); assert!( result.contains("support type") || result.contains("entity name type"), "Swift Int 类型未被识别为类型: {}", result ); assert!( result.contains("string"), "Swift 字符串未被识别: {}", result ); } #[test] fn highlight_code_typescript_keywords_and_types() { // TS 关键字 interface/const/=> 与类型 string/number 应被识别。 let code = "interface User { name: string; }\nconst x: number = 42;"; let result = highlight_code(code, Some("typescript")); assert!( result.contains("keyword"), "TypeScript 关键字未被识别: {}", result ); assert!( result.contains("support type") || result.contains("entity name type"), "TypeScript 类型未被识别: {}", result ); } #[test] fn highlight_code_jsx_tags_and_attributes() { // JSX 标签名与属性名都应被识别。 let code = "const el = ;"; for lang in &["jsx", "tsx"] { let result = highlight_code(code, Some(lang)); assert!( result.contains("entity name tag"), "{lang} JSX 标签名未识别: {result}" ); assert!( result.contains("attribute"), "{lang} JSX 属性名未识别: {result}" ); } } #[test] fn highlight_code_typescript_resolves_ts_alias() { // 别名 "ts" 与 "typescript" 输出应一致。 let code = "const x: number = 1;"; let by_ext = highlight_code(code, Some("ts")); let by_name = highlight_code(code, Some("typescript")); assert_eq!(by_ext, by_name); assert!(by_ext.contains("keyword")); } #[test] fn highlight_code_zig_keywords_and_fn() { // Zig 关键字 const/fn/pub 与内建函数 @import 都应被高亮。 let code = "const std = @import(\"std\");\npub fn main() void {}"; let result = highlight_code(code, Some("zig")); assert!(result.contains("keyword"), "Zig 关键字未被识别: {}", result); assert!( result.contains("name function"), "Zig 函数名未被识别: {}", result ); assert!( result.contains("builtin") || result.contains("support function"), "Zig 内建函数 @import 未被识别: {}", result ); } #[test] fn highlight_code_zig_types_and_strings() { // Zig 整数类型、字符串字面量与十六进制数字应被识别。 let code = "const x: u32 = 0xFF;\nconst s = \"hello\""; let result = highlight_code(code, Some("zig")); assert!( result.contains("support type") || result.contains("keyword"), "Zig u32 类型未被识别: {}", result ); assert!(result.contains("string"), "Zig 字符串未被识别: {}", result); assert!(result.contains("numeric"), "Zig 数字未被识别: {}", result); } }