Skip to main content

oxidoc_highlight/
lib.rs

1pub mod escape;
2mod lang;
3pub mod scanner;
4pub mod token;
5
6use token::render;
7
8/// Highlight `code` in the given language, returning HTML with `<span class="tok-*">` tokens.
9///
10/// Unknown languages return HTML-escaped plain text (no spans).
11/// Empty input returns an empty string.
12pub 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
20/// List all supported language identifiers.
21pub fn supported_languages() -> Vec<&'static str> {
22    lang::supported()
23}
24
25/// Check if a language is supported.
26pub fn is_supported(lang: &str) -> bool {
27    lang::get_scanner(lang).is_some()
28}
29
30// ── Wasm bindings ────────────────────────────────────────────────────
31
32#[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"), "&lt;b&gt;hi&lt;/b&gt;");
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        // Strip all spans
74        let stripped = html
75            .replace(|_: char| false, "") // no-op
76            .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        // The stripped text should equal the HTML-escaped original
88        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        // Should not panic on unicode input
109        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}