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