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