Skip to main content

slt/
syntax.rs

1//! Tree-sitter based syntax highlighting.
2//!
3//! When one of the `syntax-*` features is enabled, [`highlight_code`] uses
4//! tree-sitter grammars for accurate, language-aware highlighting.
5//! Without those features the function always returns `None` so callers
6//! can fall back to the built-in keyword highlighter.
7
8use crate::style::{Color, Style, Theme};
9
10/// Ordered list of tree-sitter highlight capture names.
11///
12/// The index of each name corresponds to the `Highlight` index
13/// returned by `HighlightEvent::HighlightStart`.
14#[cfg(any(
15    feature = "syntax-rust",
16    feature = "syntax-python",
17    feature = "syntax-javascript",
18    feature = "syntax-typescript",
19    feature = "syntax-go",
20    feature = "syntax-bash",
21    feature = "syntax-json",
22    feature = "syntax-toml",
23    feature = "syntax-c",
24    feature = "syntax-cpp",
25    feature = "syntax-java",
26    feature = "syntax-ruby",
27    feature = "syntax-css",
28    feature = "syntax-html",
29    feature = "syntax-yaml",
30))]
31const HIGHLIGHT_NAMES: &[&str] = &[
32    "attribute",
33    "comment",
34    "constant",
35    "constant.builtin",
36    "constructor",
37    "embedded",
38    "function",
39    "function.builtin",
40    "function.macro",
41    "keyword",
42    "module",
43    "number",
44    "operator",
45    "property",
46    "property.builtin",
47    "punctuation",
48    "punctuation.bracket",
49    "punctuation.delimiter",
50    "punctuation.special",
51    "string",
52    "string.special",
53    "tag",
54    "type",
55    "type.builtin",
56    "variable",
57    "variable.builtin",
58    "variable.parameter",
59];
60
61#[cfg(any(
62    feature = "syntax-rust",
63    feature = "syntax-python",
64    feature = "syntax-javascript",
65    feature = "syntax-typescript",
66    feature = "syntax-go",
67    feature = "syntax-bash",
68    feature = "syntax-json",
69    feature = "syntax-toml",
70    feature = "syntax-c",
71    feature = "syntax-cpp",
72    feature = "syntax-java",
73    feature = "syntax-ruby",
74    feature = "syntax-css",
75    feature = "syntax-html",
76    feature = "syntax-yaml",
77))]
78use std::sync::OnceLock;
79
80#[cfg(any(
81    feature = "syntax-rust",
82    feature = "syntax-python",
83    feature = "syntax-javascript",
84    feature = "syntax-typescript",
85    feature = "syntax-go",
86    feature = "syntax-bash",
87    feature = "syntax-json",
88    feature = "syntax-toml",
89    feature = "syntax-c",
90    feature = "syntax-cpp",
91    feature = "syntax-java",
92    feature = "syntax-ruby",
93    feature = "syntax-css",
94    feature = "syntax-html",
95    feature = "syntax-yaml",
96))]
97use tree_sitter_highlight::HighlightConfiguration;
98
99/// Return a cached `HighlightConfiguration` for `lang`, or `None` if the
100/// language is unsupported or the corresponding feature is not enabled.
101#[cfg(any(
102    feature = "syntax-rust",
103    feature = "syntax-python",
104    feature = "syntax-javascript",
105    feature = "syntax-typescript",
106    feature = "syntax-go",
107    feature = "syntax-bash",
108    feature = "syntax-json",
109    feature = "syntax-toml",
110    feature = "syntax-c",
111    feature = "syntax-cpp",
112    feature = "syntax-java",
113    feature = "syntax-ruby",
114    feature = "syntax-css",
115    feature = "syntax-html",
116    feature = "syntax-yaml",
117))]
118fn get_config(lang: &str) -> Option<&'static HighlightConfiguration> {
119    match lang {
120        #[cfg(feature = "syntax-rust")]
121        "rust" | "rs" => {
122            static CFG: OnceLock<HighlightConfiguration> = OnceLock::new();
123            Some(CFG.get_or_init(|| {
124                let mut c = HighlightConfiguration::new(
125                    tree_sitter_rust::LANGUAGE.into(),
126                    "rust",
127                    tree_sitter_rust::HIGHLIGHTS_QUERY,
128                    tree_sitter_rust::INJECTIONS_QUERY,
129                    "",
130                )
131                .expect("valid rust highlight config");
132                c.configure(HIGHLIGHT_NAMES);
133                c
134            }))
135        }
136
137        #[cfg(feature = "syntax-python")]
138        "python" | "py" => {
139            static CFG: OnceLock<HighlightConfiguration> = OnceLock::new();
140            Some(CFG.get_or_init(|| {
141                let mut c = HighlightConfiguration::new(
142                    tree_sitter_python::LANGUAGE.into(),
143                    "python",
144                    tree_sitter_python::HIGHLIGHTS_QUERY,
145                    "",
146                    "",
147                )
148                .expect("valid python highlight config");
149                c.configure(HIGHLIGHT_NAMES);
150                c
151            }))
152        }
153
154        #[cfg(feature = "syntax-javascript")]
155        "javascript" | "js" | "jsx" => {
156            static CFG: OnceLock<HighlightConfiguration> = OnceLock::new();
157            Some(CFG.get_or_init(|| {
158                let mut c = HighlightConfiguration::new(
159                    tree_sitter_javascript::LANGUAGE.into(),
160                    "javascript",
161                    tree_sitter_javascript::HIGHLIGHT_QUERY,
162                    tree_sitter_javascript::INJECTIONS_QUERY,
163                    tree_sitter_javascript::LOCALS_QUERY,
164                )
165                .expect("valid javascript highlight config");
166                c.configure(HIGHLIGHT_NAMES);
167                c
168            }))
169        }
170
171        #[cfg(feature = "syntax-go")]
172        "go" | "golang" => {
173            static CFG: OnceLock<HighlightConfiguration> = OnceLock::new();
174            Some(CFG.get_or_init(|| {
175                let mut c = HighlightConfiguration::new(
176                    tree_sitter_go::LANGUAGE.into(),
177                    "go",
178                    tree_sitter_go::HIGHLIGHTS_QUERY,
179                    "",
180                    "",
181                )
182                .expect("valid go highlight config");
183                c.configure(HIGHLIGHT_NAMES);
184                c
185            }))
186        }
187
188        #[cfg(feature = "syntax-bash")]
189        "bash" | "sh" | "shell" | "zsh" => {
190            static CFG: OnceLock<HighlightConfiguration> = OnceLock::new();
191            Some(CFG.get_or_init(|| {
192                let mut c = HighlightConfiguration::new(
193                    tree_sitter_bash::LANGUAGE.into(),
194                    "bash",
195                    tree_sitter_bash::HIGHLIGHT_QUERY,
196                    "",
197                    "",
198                )
199                .expect("valid bash highlight config");
200                c.configure(HIGHLIGHT_NAMES);
201                c
202            }))
203        }
204
205        #[cfg(feature = "syntax-json")]
206        "json" | "jsonc" => {
207            static CFG: OnceLock<HighlightConfiguration> = OnceLock::new();
208            Some(CFG.get_or_init(|| {
209                let mut c = HighlightConfiguration::new(
210                    tree_sitter_json::LANGUAGE.into(),
211                    "json",
212                    tree_sitter_json::HIGHLIGHTS_QUERY,
213                    "",
214                    "",
215                )
216                .expect("valid json highlight config");
217                c.configure(HIGHLIGHT_NAMES);
218                c
219            }))
220        }
221
222        #[cfg(feature = "syntax-toml")]
223        "toml" => {
224            static CFG: OnceLock<HighlightConfiguration> = OnceLock::new();
225            Some(CFG.get_or_init(|| {
226                let mut c = HighlightConfiguration::new(
227                    tree_sitter_toml_ng::LANGUAGE.into(),
228                    "toml",
229                    tree_sitter_toml_ng::HIGHLIGHTS_QUERY,
230                    "",
231                    "",
232                )
233                .expect("valid toml highlight config");
234                c.configure(HIGHLIGHT_NAMES);
235                c
236            }))
237        }
238
239        #[cfg(feature = "syntax-c")]
240        "c" | "h" => {
241            static CFG: OnceLock<HighlightConfiguration> = OnceLock::new();
242            Some(CFG.get_or_init(|| {
243                let mut c = HighlightConfiguration::new(
244                    tree_sitter_c::LANGUAGE.into(),
245                    "c",
246                    tree_sitter_c::HIGHLIGHT_QUERY,
247                    "",
248                    "",
249                )
250                .expect("valid c highlight config");
251                c.configure(HIGHLIGHT_NAMES);
252                c
253            }))
254        }
255
256        #[cfg(feature = "syntax-cpp")]
257        "cpp" | "c++" | "cxx" | "cc" | "hpp" => {
258            static CFG: OnceLock<HighlightConfiguration> = OnceLock::new();
259            Some(CFG.get_or_init(|| {
260                #[cfg(feature = "syntax-c")]
261                let highlights = {
262                    let mut combined = String::with_capacity(
263                        tree_sitter_c::HIGHLIGHT_QUERY.len()
264                            + tree_sitter_cpp::HIGHLIGHT_QUERY.len()
265                            + 1,
266                    );
267                    combined.push_str(tree_sitter_c::HIGHLIGHT_QUERY);
268                    combined.push('\n');
269                    combined.push_str(tree_sitter_cpp::HIGHLIGHT_QUERY);
270                    combined
271                };
272                #[cfg(not(feature = "syntax-c"))]
273                let highlights = tree_sitter_cpp::HIGHLIGHT_QUERY.to_string();
274
275                let mut c = HighlightConfiguration::new(
276                    tree_sitter_cpp::LANGUAGE.into(),
277                    "cpp",
278                    &highlights,
279                    "",
280                    "",
281                )
282                .expect("valid cpp highlight config");
283                c.configure(HIGHLIGHT_NAMES);
284                c
285            }))
286        }
287
288        #[cfg(feature = "syntax-typescript")]
289        "typescript" | "ts" => {
290            static CFG: OnceLock<HighlightConfiguration> = OnceLock::new();
291            Some(CFG.get_or_init(|| {
292                let mut c = HighlightConfiguration::new(
293                    tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
294                    "typescript",
295                    tree_sitter_typescript::HIGHLIGHTS_QUERY,
296                    tree_sitter_typescript::LOCALS_QUERY,
297                    "",
298                )
299                .expect("valid typescript highlight config");
300                c.configure(HIGHLIGHT_NAMES);
301                c
302            }))
303        }
304
305        #[cfg(feature = "syntax-typescript")]
306        "tsx" => {
307            static CFG: OnceLock<HighlightConfiguration> = OnceLock::new();
308            Some(CFG.get_or_init(|| {
309                let mut c = HighlightConfiguration::new(
310                    tree_sitter_typescript::LANGUAGE_TSX.into(),
311                    "tsx",
312                    tree_sitter_typescript::HIGHLIGHTS_QUERY,
313                    tree_sitter_typescript::LOCALS_QUERY,
314                    "",
315                )
316                .expect("valid tsx highlight config");
317                c.configure(HIGHLIGHT_NAMES);
318                c
319            }))
320        }
321
322        #[cfg(feature = "syntax-java")]
323        "java" => {
324            static CFG: OnceLock<HighlightConfiguration> = OnceLock::new();
325            Some(CFG.get_or_init(|| {
326                let mut c = HighlightConfiguration::new(
327                    tree_sitter_java::LANGUAGE.into(),
328                    "java",
329                    tree_sitter_java::HIGHLIGHTS_QUERY,
330                    "",
331                    "",
332                )
333                .expect("valid java highlight config");
334                c.configure(HIGHLIGHT_NAMES);
335                c
336            }))
337        }
338
339        #[cfg(feature = "syntax-ruby")]
340        "ruby" | "rb" => {
341            static CFG: OnceLock<HighlightConfiguration> = OnceLock::new();
342            Some(CFG.get_or_init(|| {
343                let mut c = HighlightConfiguration::new(
344                    tree_sitter_ruby::LANGUAGE.into(),
345                    "ruby",
346                    tree_sitter_ruby::HIGHLIGHTS_QUERY,
347                    tree_sitter_ruby::LOCALS_QUERY,
348                    "",
349                )
350                .expect("valid ruby highlight config");
351                c.configure(HIGHLIGHT_NAMES);
352                c
353            }))
354        }
355
356        #[cfg(feature = "syntax-css")]
357        "css" => {
358            static CFG: OnceLock<HighlightConfiguration> = OnceLock::new();
359            Some(CFG.get_or_init(|| {
360                let mut c = HighlightConfiguration::new(
361                    tree_sitter_css::LANGUAGE.into(),
362                    "css",
363                    tree_sitter_css::HIGHLIGHTS_QUERY,
364                    "",
365                    "",
366                )
367                .expect("valid css highlight config");
368                c.configure(HIGHLIGHT_NAMES);
369                c
370            }))
371        }
372
373        #[cfg(feature = "syntax-html")]
374        "html" | "htm" => {
375            static CFG: OnceLock<HighlightConfiguration> = OnceLock::new();
376            Some(CFG.get_or_init(|| {
377                let mut c = HighlightConfiguration::new(
378                    tree_sitter_html::LANGUAGE.into(),
379                    "html",
380                    tree_sitter_html::HIGHLIGHTS_QUERY,
381                    tree_sitter_html::INJECTIONS_QUERY,
382                    "",
383                )
384                .expect("valid html highlight config");
385                c.configure(HIGHLIGHT_NAMES);
386                c
387            }))
388        }
389
390        #[cfg(feature = "syntax-yaml")]
391        "yaml" | "yml" => {
392            static CFG: OnceLock<HighlightConfiguration> = OnceLock::new();
393            Some(CFG.get_or_init(|| {
394                let mut c = HighlightConfiguration::new(
395                    tree_sitter_yaml::LANGUAGE.into(),
396                    "yaml",
397                    tree_sitter_yaml::HIGHLIGHTS_QUERY,
398                    "",
399                    "",
400                )
401                .expect("valid yaml highlight config");
402                c.configure(HIGHLIGHT_NAMES);
403                c
404            }))
405        }
406
407        _ => None,
408    }
409}
410
411/// Map a tree-sitter highlight capture name to an SLT [`Style`].
412///
413/// Colors follow the One Dark palette and flip between light/dark variants
414/// based on [`Theme::is_dark`].
415#[cfg(any(
416    feature = "syntax-rust",
417    feature = "syntax-python",
418    feature = "syntax-javascript",
419    feature = "syntax-typescript",
420    feature = "syntax-go",
421    feature = "syntax-bash",
422    feature = "syntax-json",
423    feature = "syntax-toml",
424    feature = "syntax-c",
425    feature = "syntax-cpp",
426    feature = "syntax-java",
427    feature = "syntax-ruby",
428    feature = "syntax-css",
429    feature = "syntax-html",
430    feature = "syntax-yaml",
431))]
432fn highlight_name_to_style(name: &str, theme: &Theme) -> Style {
433    let dark = theme.is_dark;
434    match name {
435        "keyword" => Style::new().fg(if dark {
436            Color::Rgb(198, 120, 221)
437        } else {
438            Color::Rgb(166, 38, 164)
439        }),
440        "string" | "string.special" => Style::new().fg(if dark {
441            Color::Rgb(152, 195, 121)
442        } else {
443            Color::Rgb(80, 161, 79)
444        }),
445        "comment" => Style::new().fg(theme.text_dim).italic(),
446        "number" | "constant" | "constant.builtin" => Style::new().fg(if dark {
447            Color::Rgb(209, 154, 102)
448        } else {
449            Color::Rgb(152, 104, 1)
450        }),
451        "function" | "function.builtin" => Style::new().fg(if dark {
452            Color::Rgb(97, 175, 239)
453        } else {
454            Color::Rgb(64, 120, 242)
455        }),
456        "function.macro" => Style::new().fg(if dark {
457            Color::Rgb(86, 182, 194)
458        } else {
459            Color::Rgb(1, 132, 188)
460        }),
461        "type" | "type.builtin" | "constructor" => Style::new().fg(if dark {
462            Color::Rgb(229, 192, 123)
463        } else {
464            Color::Rgb(152, 104, 1)
465        }),
466        "variable.builtin" => Style::new().fg(if dark {
467            Color::Rgb(224, 108, 117)
468        } else {
469            Color::Rgb(166, 38, 164)
470        }),
471        "property" | "property.builtin" => Style::new().fg(if dark {
472            Color::Rgb(97, 175, 239)
473        } else {
474            Color::Rgb(64, 120, 242)
475        }),
476        "tag" => Style::new().fg(if dark {
477            Color::Rgb(224, 108, 117)
478        } else {
479            Color::Rgb(166, 38, 164)
480        }),
481        "attribute" => Style::new().fg(if dark {
482            Color::Rgb(209, 154, 102)
483        } else {
484            Color::Rgb(152, 104, 1)
485        }),
486        "module" | "embedded" | "operator" | "variable" | "variable.parameter" => {
487            Style::new().fg(theme.text)
488        }
489        "punctuation" | "punctuation.bracket" | "punctuation.delimiter" | "punctuation.special" => {
490            Style::new().fg(theme.text_dim)
491        }
492        _ => Style::new().fg(theme.text),
493    }
494}
495
496/// Highlight source code using tree-sitter.
497///
498/// Returns `Some(lines)` where each line is a `Vec<(text, style)>` of
499/// styled segments, or `None` if:
500/// - The language is not recognised
501/// - The corresponding `syntax-*` feature is not enabled
502/// - Parsing fails
503///
504/// Callers should fall back to the built-in keyword highlighter when
505/// `None` is returned.
506///
507/// # Example
508///
509/// ```ignore
510/// let lines = slt::syntax::highlight_code("let x = 1;", "rust", &theme);
511/// ```
512#[allow(unused_variables)]
513pub fn highlight_code(code: &str, lang: &str, theme: &Theme) -> Option<Vec<Vec<(String, Style)>>> {
514    #[cfg(any(
515        feature = "syntax-rust",
516        feature = "syntax-python",
517        feature = "syntax-javascript",
518        feature = "syntax-typescript",
519        feature = "syntax-go",
520        feature = "syntax-bash",
521        feature = "syntax-json",
522        feature = "syntax-toml",
523        feature = "syntax-c",
524        feature = "syntax-cpp",
525        feature = "syntax-java",
526        feature = "syntax-ruby",
527        feature = "syntax-css",
528        feature = "syntax-html",
529        feature = "syntax-yaml",
530    ))]
531    {
532        use tree_sitter_highlight::{HighlightEvent, Highlighter};
533
534        let config = get_config(lang)?;
535        let mut highlighter = Highlighter::new();
536        let highlights = highlighter
537            .highlight(config, code.as_bytes(), None, |_| None)
538            .ok()?;
539
540        let default_style = Style::new().fg(theme.text);
541        let mut result: Vec<Vec<(String, Style)>> = Vec::new();
542        let mut current_line: Vec<(String, Style)> = Vec::new();
543        let mut style_stack: Vec<Style> = vec![default_style];
544
545        for event in highlights {
546            match event.ok()? {
547                HighlightEvent::Source { start, end } => {
548                    let text = &code[start..end];
549                    let style = *style_stack.last().unwrap_or(&default_style);
550                    // Split by newlines to produce per-line segments
551                    for (i, part) in text.split('\n').enumerate() {
552                        if i > 0 {
553                            result.push(std::mem::take(&mut current_line));
554                        }
555                        if !part.is_empty() {
556                            current_line.push((part.to_string(), style));
557                        }
558                    }
559                }
560                HighlightEvent::HighlightStart(highlight) => {
561                    let name = HIGHLIGHT_NAMES.get(highlight.0).copied().unwrap_or("");
562                    let style = highlight_name_to_style(name, theme);
563                    style_stack.push(style);
564                }
565                HighlightEvent::HighlightEnd => {
566                    style_stack.pop();
567                }
568            }
569        }
570
571        if !current_line.is_empty() {
572            result.push(current_line);
573        }
574
575        Some(result)
576    }
577
578    #[cfg(not(any(
579        feature = "syntax-rust",
580        feature = "syntax-python",
581        feature = "syntax-javascript",
582        feature = "syntax-typescript",
583        feature = "syntax-go",
584        feature = "syntax-bash",
585        feature = "syntax-json",
586        feature = "syntax-toml",
587        feature = "syntax-c",
588        feature = "syntax-cpp",
589        feature = "syntax-java",
590        feature = "syntax-ruby",
591        feature = "syntax-css",
592        feature = "syntax-html",
593        feature = "syntax-yaml",
594    )))]
595    {
596        None
597    }
598}
599
600/// Returns `true` if tree-sitter highlighting is available for `lang`.
601///
602/// This checks both that the corresponding `syntax-*` feature is enabled
603/// and that the language string is recognised.
604#[allow(unused_variables)]
605pub fn is_language_supported(lang: &str) -> bool {
606    #[cfg(any(
607        feature = "syntax-rust",
608        feature = "syntax-python",
609        feature = "syntax-javascript",
610        feature = "syntax-typescript",
611        feature = "syntax-go",
612        feature = "syntax-bash",
613        feature = "syntax-json",
614        feature = "syntax-toml",
615        feature = "syntax-c",
616        feature = "syntax-cpp",
617        feature = "syntax-java",
618        feature = "syntax-ruby",
619        feature = "syntax-css",
620        feature = "syntax-html",
621        feature = "syntax-yaml",
622    ))]
623    {
624        get_config(lang).is_some()
625    }
626    #[cfg(not(any(
627        feature = "syntax-rust",
628        feature = "syntax-python",
629        feature = "syntax-javascript",
630        feature = "syntax-typescript",
631        feature = "syntax-go",
632        feature = "syntax-bash",
633        feature = "syntax-json",
634        feature = "syntax-toml",
635        feature = "syntax-c",
636        feature = "syntax-cpp",
637        feature = "syntax-java",
638        feature = "syntax-ruby",
639        feature = "syntax-css",
640        feature = "syntax-html",
641        feature = "syntax-yaml",
642    )))]
643    {
644        false
645    }
646}
647
648#[cfg(test)]
649mod tests {
650    use super::*;
651    use crate::style::Theme;
652
653    #[test]
654    fn highlight_returns_none_for_unknown_lang() {
655        let theme = Theme::dark();
656        assert!(highlight_code("let x = 1;", "brainfuck", &theme).is_none());
657    }
658
659    #[test]
660    fn is_language_supported_unknown() {
661        assert!(!is_language_supported("haskell"));
662    }
663
664    #[cfg(feature = "syntax-rust")]
665    #[test]
666    fn highlight_rust_basic() {
667        let theme = Theme::dark();
668        let result = highlight_code("let x = 1;", "rust", &theme);
669        assert!(result.is_some());
670        let lines = result.unwrap();
671        assert_eq!(lines.len(), 1);
672        // "let" should be in the first line's segments
673        let flat: String = lines[0].iter().map(|(t, _)| t.as_str()).collect();
674        assert!(flat.contains("let"));
675        assert!(flat.contains("1"));
676    }
677
678    #[cfg(feature = "syntax-rust")]
679    #[test]
680    fn highlight_rust_multiline() {
681        let theme = Theme::dark();
682        let code = "fn main() {\n    println!(\"hello\");\n}";
683        let result = highlight_code(code, "rust", &theme).unwrap();
684        assert_eq!(result.len(), 3);
685    }
686
687    #[cfg(feature = "syntax-rust")]
688    #[test]
689    fn highlight_rust_rs_alias() {
690        let theme = Theme::dark();
691        assert!(highlight_code("let x = 1;", "rs", &theme).is_some());
692    }
693
694    #[cfg(feature = "syntax-python")]
695    #[test]
696    fn highlight_python_basic() {
697        let theme = Theme::dark();
698        let result = highlight_code("def foo():\n    return 42", "python", &theme);
699        assert!(result.is_some());
700        let lines = result.unwrap();
701        assert_eq!(lines.len(), 2);
702    }
703
704    #[cfg(feature = "syntax-javascript")]
705    #[test]
706    fn highlight_javascript_basic() {
707        let theme = Theme::dark();
708        let result = highlight_code("const x = () => 42;", "js", &theme);
709        assert!(result.is_some());
710    }
711
712    #[cfg(feature = "syntax-bash")]
713    #[test]
714    fn highlight_bash_basic() {
715        let theme = Theme::dark();
716        let result = highlight_code("echo \"hello\"", "sh", &theme);
717        assert!(result.is_some());
718    }
719
720    #[cfg(feature = "syntax-json")]
721    #[test]
722    fn highlight_json_basic() {
723        let theme = Theme::dark();
724        let result = highlight_code("{\"key\": 42}", "json", &theme);
725        assert!(result.is_some());
726    }
727
728    #[cfg(feature = "syntax-toml")]
729    #[test]
730    fn highlight_toml_basic() {
731        let theme = Theme::dark();
732        let result = highlight_code("[package]\nname = \"slt\"", "toml", &theme);
733        assert!(result.is_some());
734    }
735
736    #[cfg(feature = "syntax-go")]
737    #[test]
738    fn highlight_go_basic() {
739        let theme = Theme::dark();
740        let result = highlight_code("package main\nfunc main() {}", "go", &theme);
741        assert!(result.is_some());
742    }
743
744    #[cfg(feature = "syntax-rust")]
745    #[test]
746    fn highlight_light_theme_differs() {
747        let dark = Theme::dark();
748        let light = Theme::light();
749        let dark_result = highlight_code("let x = 1;", "rust", &dark).unwrap();
750        let light_result = highlight_code("let x = 1;", "rust", &light).unwrap();
751        // Keyword styles should differ between dark and light
752        let dark_styles: Vec<Style> = dark_result[0].iter().map(|(_, s)| *s).collect();
753        let light_styles: Vec<Style> = light_result[0].iter().map(|(_, s)| *s).collect();
754        assert_ne!(dark_styles, light_styles);
755    }
756
757    #[cfg(feature = "syntax-rust")]
758    #[test]
759    fn highlight_incomplete_code_does_not_panic() {
760        let theme = Theme::dark();
761        let result = highlight_code("fn main( {", "rust", &theme);
762        assert!(result.is_some());
763    }
764
765    #[cfg(feature = "syntax-c")]
766    #[test]
767    fn highlight_c_basic() {
768        let theme = Theme::dark();
769        assert!(
770            highlight_code("#include <stdio.h>\nint main() { return 0; }", "c", &theme).is_some()
771        );
772    }
773
774    #[cfg(feature = "syntax-cpp")]
775    #[test]
776    fn highlight_cpp_basic() {
777        let theme = Theme::dark();
778        assert!(highlight_code("class Foo { public: void bar(); };", "cpp", &theme).is_some());
779    }
780
781    #[cfg(feature = "syntax-typescript")]
782    #[test]
783    fn highlight_typescript_basic() {
784        let theme = Theme::dark();
785        assert!(highlight_code("const x: number = 42;", "ts", &theme).is_some());
786    }
787
788    #[cfg(feature = "syntax-typescript")]
789    #[test]
790    fn highlight_tsx_basic() {
791        let theme = Theme::dark();
792        assert!(highlight_code("const App = () => <div>hello</div>;", "tsx", &theme).is_some());
793    }
794
795    #[cfg(feature = "syntax-java")]
796    #[test]
797    fn highlight_java_basic() {
798        let theme = Theme::dark();
799        assert!(highlight_code(
800            "public class Main { public static void main(String[] args) {} }",
801            "java",
802            &theme
803        )
804        .is_some());
805    }
806
807    #[cfg(feature = "syntax-ruby")]
808    #[test]
809    fn highlight_ruby_basic() {
810        let theme = Theme::dark();
811        assert!(highlight_code("def hello\n  puts 'world'\nend", "ruby", &theme).is_some());
812    }
813
814    #[cfg(feature = "syntax-css")]
815    #[test]
816    fn highlight_css_basic() {
817        let theme = Theme::dark();
818        assert!(highlight_code("body { color: red; }", "css", &theme).is_some());
819    }
820
821    #[cfg(feature = "syntax-html")]
822    #[test]
823    fn highlight_html_basic() {
824        let theme = Theme::dark();
825        assert!(highlight_code("<div class=\"test\">hello</div>", "html", &theme).is_some());
826    }
827
828    #[cfg(feature = "syntax-yaml")]
829    #[test]
830    fn highlight_yaml_basic() {
831        let theme = Theme::dark();
832        assert!(highlight_code("name: slt\nversion: 0.14", "yaml", &theme).is_some());
833    }
834}