1pub mod escape;
2mod lang;
3pub mod scanner;
4pub mod token;
5
6use token::render;
7
8pub fn highlight(code: &str, lang: &str) -> String {
13 if code.is_empty() {
14 return String::new();
15 }
16 let tokens = lang::scan(code, lang);
17 render(code, &tokens)
18}
19
20pub fn supported_languages() -> Vec<&'static str> {
22 lang::supported()
23}
24
25pub fn is_supported(lang: &str) -> bool {
27 lang::get_scanner(lang).is_some()
28}
29
30#[cfg(feature = "wasm")]
33mod wasm {
34 use wasm_bindgen::prelude::*;
35
36 #[wasm_bindgen(js_name = "highlight")]
37 pub fn highlight_wasm(code: &str, lang: &str) -> String {
38 crate::highlight(code, lang)
39 }
40
41 #[wasm_bindgen(js_name = "supportedLanguages")]
42 pub fn supported_languages_wasm() -> Vec<String> {
43 crate::supported_languages()
44 .into_iter()
45 .map(String::from)
46 .collect()
47 }
48
49 #[wasm_bindgen(js_name = "isSupported")]
50 pub fn is_supported_wasm(lang: &str) -> bool {
51 crate::is_supported(lang)
52 }
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58
59 #[test]
60 fn empty_input() {
61 assert_eq!(highlight("", "rust"), "");
62 }
63
64 #[test]
65 fn unknown_language_escapes_html() {
66 assert_eq!(highlight("<b>hi</b>", "unknown"), "<b>hi</b>");
67 }
68
69 #[test]
70 fn round_trip_strip_spans() {
71 let code = "let x = 42; // test\nfn foo() {}";
72 let html = highlight(code, "rust");
73 let stripped = html
75 .replace(|_: char| false, "") .split("<span")
77 .map(|s| {
78 if let Some(pos) = s.find('>') {
79 &s[pos + 1..]
80 } else {
81 s
82 }
83 })
84 .collect::<Vec<_>>()
85 .join("")
86 .replace("</span>", "");
87 let mut expected = String::new();
89 crate::escape::escape_html(code, &mut expected);
90 assert_eq!(stripped, expected);
91 }
92
93 #[test]
94 fn supported_languages_nonempty() {
95 assert!(supported_languages().len() > 10);
96 }
97
98 #[test]
99 fn is_supported_works() {
100 assert!(is_supported("rust"));
101 assert!(is_supported("rs"));
102 assert!(is_supported("javascript"));
103 assert!(!is_supported("brainfuck"));
104 }
105
106 #[test]
107 fn unicode_no_panic() {
108 let _ = highlight("let 变量 = \"你好世界\";", "rust");
110 let _ = highlight("const 🎉 = true;", "javascript");
111 }
112
113 #[test]
114 fn crlf_handling() {
115 let code = "let x = 1;\r\nlet y = 2;\r\n";
116 let html = highlight(code, "rust");
117 assert!(html.contains("tok-keyword"));
118 assert!(html.contains("\r\n"));
119 }
120
121 #[test]
122 fn spec_example() {
123 let out = highlight("let x = 42;", "rust");
124 assert!(out.contains("<span class=\"tok-keyword\">let</span>"));
125 assert!(out.contains("<span class=\"tok-operator\">=</span>"));
126 assert!(out.contains("<span class=\"tok-number\">42</span>"));
127 assert!(out.contains("<span class=\"tok-punctuation\">;</span>"));
128 }
129}